diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index b7c549d14..11a925f8f 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -24,7 +24,6 @@ Otherwise, link the JIRA ticket. Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields. --> - [ ] Added or updated unit tests (required for all new features) -- [ ] All new/modified APIs include changes to [mattermost-redux](https://github.com/mattermost/mattermost-redux) (please link) - [ ] Has UI changes - [ ] Includes text changes and localization file updates diff --git a/app/actions/helpers/channels.ts b/app/actions/helpers/channels.ts index 3e05b574b..c33b17273 100644 --- a/app/actions/helpers/channels.ts +++ b/app/actions/helpers/channels.ts @@ -1,22 +1,22 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from 'mattermost-redux/action_types'; -import {Client4} from 'mattermost-redux/client'; -import {General, Preferences} from 'mattermost-redux/constants'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId, getUsers, getUserIdsInChannels} from 'mattermost-redux/selectors/entities/users'; -import {getUserIdFromChannelName, isAutoClosed} from 'mattermost-redux/utils/channel_utils'; -import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils'; +import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types'; +import {Client4} from '@mm-redux/client'; +import {General, Preferences} from '@mm-redux/constants'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getMyPreferences} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users'; +import {getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; -import {ActionResult, GenericAction} from 'mattermost-redux/types/actions'; -import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'; -import {PreferenceType} from 'mattermost-redux/types/preferences'; -import {GlobalState} from 'mattermost-redux/types/store'; -import {UserProfile} from 'mattermost-redux/types/users'; -import {RelationOneToMany} from 'mattermost-redux/types/utilities'; +import {ActionResult, GenericAction} from '@mm-redux/types/actions'; +import {Channel, ChannelMembership} from '@mm-redux/types/channels'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {GlobalState} from '@mm-redux/types/store'; +import {UserProfile} from '@mm-redux/types/users'; +import {RelationOneToMany} from '@mm-redux/types/utilities'; import {isDirectChannelVisible, isGroupChannelVisible} from '@utils/channels'; import {buildPreference} from '@utils/preferences'; diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js index f41372a82..54db7f846 100644 --- a/app/actions/navigation/index.js +++ b/app/actions/navigation/index.js @@ -5,7 +5,7 @@ import {Keyboard, Platform} from 'react-native'; import {Navigation} from 'react-native-navigation'; import merge from 'deepmerge'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import store from 'app/store'; import EphemeralStore from 'app/store/ephemeral_store'; diff --git a/app/actions/navigation/index.test.js b/app/actions/navigation/index.test.js index d8d6cfc09..645342eb9 100644 --- a/app/actions/navigation/index.test.js +++ b/app/actions/navigation/index.test.js @@ -6,7 +6,7 @@ import {Navigation} from 'react-native-navigation'; import merge from 'deepmerge'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import EphemeralStore from 'app/store/ephemeral_store'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 730dc6f0a..f251f99a9 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -5,30 +5,30 @@ import {batchActions} from 'redux-batched-actions'; import {ViewTypes} from 'app/constants'; -import {ChannelTypes, RoleTypes} from 'mattermost-redux/action_types'; +import {ChannelTypes, RoleTypes} from '@mm-redux/action_types'; import { fetchMyChannelsAndMembers, getChannelByNameAndTeamName, leaveChannel as serviceLeaveChannel, -} from 'mattermost-redux/actions/channels'; -import {getFilesForPost} from 'mattermost-redux/actions/files'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {selectTeam} from 'mattermost-redux/actions/teams'; -import {Client4} from 'mattermost-redux/client'; -import {General, Preferences} from 'mattermost-redux/constants'; -import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts'; +} from '@mm-redux/actions/channels'; +import {getFilesForPost} from '@mm-redux/actions/files'; +import {savePreferences} from '@mm-redux/actions/preferences'; +import {selectTeam} from '@mm-redux/actions/teams'; +import {Client4} from '@mm-redux/client'; +import {General, Preferences} from '@mm-redux/constants'; +import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts'; import { getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, isManuallyUnread, -} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getTeamByName} from 'mattermost-redux/selectors/entities/teams'; +} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getTeamByName} from '@mm-redux/selectors/entities/teams'; -import {getChannelByName as getChannelByNameSelector} from 'mattermost-redux/utils/channel_utils'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {getLastCreateAt} from 'mattermost-redux/utils/post_utils'; +import {getChannelByName as getChannelByNameSelector} from '@mm-redux/utils/channel_utils'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {getLastCreateAt} from '@mm-redux/utils/post_utils'; import {loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels'; import {getPosts, getPostsBefore, getPostsSince, getPostThread} from '@actions/views/post'; diff --git a/app/actions/views/channel.test.js b/app/actions/views/channel.test.js index 4280d0058..7e6151405 100644 --- a/app/actions/views/channel.test.js +++ b/app/actions/views/channel.test.js @@ -5,7 +5,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import initialState from 'app/initial_state'; -import {ChannelTypes} from 'mattermost-redux/action_types'; +import {ChannelTypes} from '@mm-redux/action_types'; import testHelper from 'test/test_helper'; import * as ChannelActions from 'app/actions/views/channel'; @@ -15,13 +15,13 @@ const { loadPostsIfNecessaryWithRetry, } = ChannelActions; -import postReducer from 'mattermost-redux/reducers/entities/posts'; +import postReducer from '@mm-redux/reducers/entities/posts'; const MOCK_CHANNEL_MARK_AS_READ = 'MOCK_CHANNEL_MARK_AS_READ'; const MOCK_CHANNEL_MARK_AS_VIEWED = 'MOCK_CHANNEL_MARK_AS_VIEWED'; -jest.mock('mattermost-redux/actions/channels', () => { - const channelActions = require.requireActual('mattermost-redux/actions/channels'); +jest.mock('@mm-redux/actions/channels', () => { + const channelActions = require.requireActual('@mm-redux/actions/channels'); return { ...channelActions, markChannelAsRead: jest.fn().mockReturnValue({type: 'MOCK_CHANNEL_MARK_AS_READ'}), @@ -29,8 +29,8 @@ jest.mock('mattermost-redux/actions/channels', () => { }; }); -jest.mock('mattermost-redux/selectors/entities/teams', () => { - const teamSelectors = require.requireActual('mattermost-redux/selectors/entities/teams'); +jest.mock('@mm-redux/selectors/entities/teams', () => { + const teamSelectors = require.requireActual('@mm-redux/selectors/entities/teams'); return { ...teamSelectors, getTeamByName: jest.fn(() => ({name: 'current-team-name'})), @@ -48,7 +48,7 @@ describe('Actions.Views.Channel', () => { const MOCK_RECEIVED_POSTS_IN_CHANNEL = 'RECEIVED_POSTS_IN_CHANNEL'; const MOCK_RECEIVED_POSTS_SINCE = 'MOCK_RECEIVED_POSTS_SINCE'; - const actions = require('mattermost-redux/actions/channels'); + const actions = require('@mm-redux/actions/channels'); actions.getChannelByNameAndTeamName = jest.fn((teamName) => { if (teamName) { return { @@ -96,7 +96,7 @@ describe('Actions.Views.Channel', () => { }; }); - const postUtils = require('mattermost-redux/utils/post_utils'); + const postUtils = require('@mm-redux/utils/post_utils'); postUtils.getLastCreateAt = jest.fn((array) => { return array[0].create_at; }); @@ -138,7 +138,7 @@ describe('Actions.Views.Channel', () => { }, }; - const channelSelectors = require('mattermost-redux/selectors/entities/channels'); + const channelSelectors = require('@mm-redux/selectors/entities/channels'); channelSelectors.getChannel = jest.fn((state, channelId) => ({data: channelId})); channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId); channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}})); diff --git a/app/actions/views/channel_add_members.js b/app/actions/views/channel_add_members.js index f09e06616..7d48ddb06 100644 --- a/app/actions/views/channel_add_members.js +++ b/app/actions/views/channel_add_members.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {addChannelMember} from 'mattermost-redux/actions/channels'; +import {addChannelMember} from '@mm-redux/actions/channels'; export function handleAddChannelMembers(channelId, members) { return async (dispatch) => { diff --git a/app/actions/views/channel_members.js b/app/actions/views/channel_members.js index 9505c407c..3d9391b5d 100644 --- a/app/actions/views/channel_members.js +++ b/app/actions/views/channel_members.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {removeChannelMember} from 'mattermost-redux/actions/channels'; +import {removeChannelMember} from '@mm-redux/actions/channels'; export function handleRemoveChannelMembers(channelId, members) { return async (dispatch, getState) => { diff --git a/app/actions/views/command.js b/app/actions/views/command.js index dfc97208c..35af82afa 100644 --- a/app/actions/views/command.js +++ b/app/actions/views/command.js @@ -1,9 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {IntegrationTypes} from 'mattermost-redux/action_types'; -import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {IntegrationTypes} from '@mm-redux/action_types'; +import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; export function executeCommand(message, channelId, rootId) { return async (dispatch, getState) => { diff --git a/app/actions/views/create_channel.js b/app/actions/views/create_channel.js index 0765059f8..12307b289 100644 --- a/app/actions/views/create_channel.js +++ b/app/actions/views/create_channel.js @@ -2,11 +2,11 @@ // See LICENSE.txt for license information. import {handleSelectChannel, setChannelDisplayName} from './channel'; -import {createChannel} from 'mattermost-redux/actions/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {cleanUpUrlable} from 'mattermost-redux/utils/channel_utils'; -import {generateId} from 'mattermost-redux/utils/helpers'; +import {createChannel} from '@mm-redux/actions/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {cleanUpUrlable} from '@mm-redux/utils/channel_utils'; +import {generateId} from '@mm-redux/utils/helpers'; export function generateChannelNameFromDisplayName(displayName) { let name = cleanUpUrlable(displayName); diff --git a/app/actions/views/edit_profile.js b/app/actions/views/edit_profile.js index 3e91b7be5..c64267027 100644 --- a/app/actions/views/edit_profile.js +++ b/app/actions/views/edit_profile.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {updateMe, setDefaultProfileImage} from 'mattermost-redux/actions/users'; +import {updateMe, setDefaultProfileImage} from '@mm-redux/actions/users'; import {ViewTypes} from 'app/constants'; diff --git a/app/actions/views/emoji.js b/app/actions/views/emoji.js index 26d95ba09..6ef1a7aae 100644 --- a/app/actions/views/emoji.js +++ b/app/actions/views/emoji.js @@ -3,10 +3,10 @@ import {batchActions} from 'redux-batched-actions'; -import {EmojiTypes} from 'mattermost-redux/action_types'; -import {addReaction as serviceAddReaction, getNeededCustomEmojis} from 'mattermost-redux/actions/posts'; -import {Client4} from 'mattermost-redux/client'; -import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts'; +import {EmojiTypes} from '@mm-redux/action_types'; +import {addReaction as serviceAddReaction, getNeededCustomEmojis} from '@mm-redux/actions/posts'; +import {Client4} from '@mm-redux/client'; +import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts'; import {ViewTypes} from 'app/constants'; diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js index 9b5784943..3f5dd1e48 100644 --- a/app/actions/views/file_upload.js +++ b/app/actions/views/file_upload.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {FileTypes} from 'mattermost-redux/action_types'; +import {FileTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; import {buildFileUploadData, generateId} from 'app/utils/file'; diff --git a/app/actions/views/login.js b/app/actions/views/login.js index 2f40e4685..7af51e0da 100644 --- a/app/actions/views/login.js +++ b/app/actions/views/login.js @@ -3,14 +3,14 @@ import moment from 'moment-timezone'; -import {getDataRetentionPolicy} from 'mattermost-redux/actions/general'; -import {GeneralTypes} from 'mattermost-redux/action_types'; -import {getSessions} from 'mattermost-redux/actions/users'; -import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; -import {Client4} from 'mattermost-redux/client'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getDataRetentionPolicy} from '@mm-redux/actions/general'; +import {GeneralTypes} from '@mm-redux/action_types'; +import {getSessions} from '@mm-redux/actions/users'; +import {autoUpdateTimezone} from '@mm-redux/actions/timezone'; +import {Client4} from '@mm-redux/client'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; import {setAppCredentials} from 'app/init/credentials'; import PushNotifications from 'app/push_notifications'; diff --git a/app/actions/views/login.test.js b/app/actions/views/login.test.js index 77ccf851a..92cc6e00f 100644 --- a/app/actions/views/login.test.js +++ b/app/actions/views/login.test.js @@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import * as GeneralActions from 'mattermost-redux/actions/general'; +import * as GeneralActions from '@mm-redux/actions/general'; import {handleSuccessfulLogin} from 'app/actions/views/login'; diff --git a/app/actions/views/more_dms.js b/app/actions/views/more_dms.js index 79180f729..250345a98 100644 --- a/app/actions/views/more_dms.js +++ b/app/actions/views/more_dms.js @@ -1,9 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils'; -import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels'; -import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users'; +import {getDirectChannelName} from '@mm-redux/utils/channel_utils'; +import {createDirectChannel, createGroupChannel} from '@mm-redux/actions/channels'; +import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users'; import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel'; export function makeDirectChannel(otherUserId, switchToChannel = true) { diff --git a/app/actions/views/post.js b/app/actions/views/post.js index b44660716..349d9e904 100644 --- a/app/actions/views/post.js +++ b/app/actions/views/post.js @@ -3,7 +3,7 @@ import {batchActions} from 'redux-batched-actions'; -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; import { doPostAction, getNeededAtMentionedUsernames, @@ -14,11 +14,11 @@ import { receivedPostsInChannel, receivedPostsSince, receivedPostsInThread, -} from 'mattermost-redux/actions/posts'; -import {Client4} from 'mattermost-redux/client'; -import {Posts} from 'mattermost-redux/constants'; -import {getPost as selectPost} from 'mattermost-redux/selectors/entities/posts'; -import {removeUserFromList} from 'mattermost-redux/utils/user_utils'; +} from '@mm-redux/actions/posts'; +import {Client4} from '@mm-redux/client'; +import {Posts} from '@mm-redux/constants'; +import {getPost as selectPost} from '@mm-redux/selectors/entities/posts'; +import {removeUserFromList} from '@mm-redux/utils/user_utils'; import {ViewTypes} from 'app/constants'; import {generateId} from 'app/utils/file'; diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 3dc498b9a..e9f2e0d2d 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -3,13 +3,13 @@ import {batchActions} from 'redux-batched-actions'; -import {ChannelTypes, GeneralTypes, TeamTypes} from 'mattermost-redux/action_types'; -import {Client4} from 'mattermost-redux/client'; -import {General} from 'mattermost-redux/constants'; -import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels'; -import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general'; -import {receivedNewPost} from 'mattermost-redux/actions/posts'; -import {getMyTeams, getMyTeamMembers} from 'mattermost-redux/actions/teams'; +import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types'; +import {Client4} from '@mm-redux/client'; +import {General} from '@mm-redux/constants'; +import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels'; +import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from '@mm-redux/actions/general'; +import {receivedNewPost} from '@mm-redux/actions/posts'; +import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams'; import {ViewTypes} from 'app/constants'; import EphemeralStore from 'app/store/ephemeral_store'; diff --git a/app/actions/views/select_server.js b/app/actions/views/select_server.js index 5defda3db..e7142a3b1 100644 --- a/app/actions/views/select_server.js +++ b/app/actions/views/select_server.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {batchActions} from 'redux-batched-actions'; -import {GeneralTypes} from 'mattermost-redux/action_types'; +import {GeneralTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/actions/views/select_server.test.js b/app/actions/views/select_server.test.js index ea64379a1..84afe3e56 100644 --- a/app/actions/views/select_server.test.js +++ b/app/actions/views/select_server.test.js @@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import {GeneralTypes} from 'mattermost-redux/action_types'; +import {GeneralTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index b69479e50..1c94b4a69 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -3,11 +3,11 @@ import {batchActions} from 'redux-batched-actions'; -import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types'; -import {getMyTeams} from 'mattermost-redux/actions/teams'; -import {RequestStatus} from 'mattermost-redux/constants'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {ChannelTypes, TeamTypes} from '@mm-redux/action_types'; +import {getMyTeams} from '@mm-redux/actions/teams'; +import {RequestStatus} from '@mm-redux/constants'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {NavigationTypes} from 'app/constants'; import {selectFirstAvailableTeam} from 'app/utils/teams'; diff --git a/app/actions/views/user.js b/app/actions/views/user.js index 4f24efaa3..d841fe82d 100644 --- a/app/actions/views/user.js +++ b/app/actions/views/user.js @@ -3,15 +3,15 @@ import {batchActions} from 'redux-batched-actions'; -import {GeneralTypes, RoleTypes, UserTypes} from 'mattermost-redux/action_types'; -import {getDataRetentionPolicy} from 'mattermost-redux/actions/general'; -import * as HelperActions from 'mattermost-redux/actions/helpers'; -import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; -import {Client4} from 'mattermost-redux/client'; -import {General} from 'mattermost-redux/constants'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; +import {GeneralTypes, RoleTypes, UserTypes} from '@mm-redux/action_types'; +import {getDataRetentionPolicy} from '@mm-redux/actions/general'; +import * as HelperActions from '@mm-redux/actions/helpers'; +import {autoUpdateTimezone} from '@mm-redux/actions/timezone'; +import {Client4} from '@mm-redux/client'; +import {General} from '@mm-redux/constants'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users'; import {setAppCredentials} from 'app/init/credentials'; import {setCSRFFromCookie} from '@utils/security'; diff --git a/app/actions/views/user.test.js b/app/actions/views/user.test.js index 41fcfee42..c4bc4f313 100644 --- a/app/actions/views/user.test.js +++ b/app/actions/views/user.test.js @@ -4,14 +4,14 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import {UserTypes} from 'mattermost-redux/action_types'; -import {General} from 'mattermost-redux/constants'; +import {UserTypes} from '@mm-redux/action_types'; +import {General} from '@mm-redux/constants'; import {setCurrentUserStatusOffline} from 'app/actions/views/user'; const mockStore = configureStore([thunk]); -jest.mock('mattermost-redux/actions/users', () => ({ +jest.mock('@mm-redux/actions/users', () => ({ getStatus: (...args) => ({type: 'MOCK_GET_STATUS', args}), getStatusesByIds: (...args) => ({type: 'MOCK_GET_STATUS_BY_IDS', args}), startPeriodicStatusUpdates: () => ({type: 'MOCK_PERIODIC_STATUS_UPDATES'}), diff --git a/app/actions/websocket.test.js b/app/actions/websocket.test.js index 31fa29963..336da5226 100644 --- a/app/actions/websocket.test.js +++ b/app/actions/websocket.test.js @@ -8,16 +8,16 @@ import {batchActions} from 'redux-batched-actions'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; -import {ChannelTypes, GeneralTypes, RoleTypes, TeamTypes, UserTypes} from 'mattermost-redux/action_types'; -import * as ChannelActions from 'mattermost-redux/actions/channels'; -import * as PostActions from 'mattermost-redux/actions/posts'; -import * as PreferenceActions from 'mattermost-redux/actions/preferences'; -import * as TeamActions from 'mattermost-redux/actions/teams'; -import * as UserActions from 'mattermost-redux/actions/users'; -import {Client4} from 'mattermost-redux/client'; -import {General, Posts, RequestStatus} from 'mattermost-redux/constants'; -import * as PostSelectors from 'mattermost-redux/selectors/entities/posts'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {ChannelTypes, GeneralTypes, RoleTypes, TeamTypes, UserTypes} from '@mm-redux/action_types'; +import * as ChannelActions from '@mm-redux/actions/channels'; +import * as PostActions from '@mm-redux/actions/posts'; +import * as PreferenceActions from '@mm-redux/actions/preferences'; +import * as TeamActions from '@mm-redux/actions/teams'; +import * as UserActions from '@mm-redux/actions/users'; +import {Client4} from '@mm-redux/client'; +import {General, Posts, RequestStatus} from '@mm-redux/constants'; +import * as PostSelectors from '@mm-redux/selectors/entities/posts'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import * as Actions from '@actions/websocket'; import {WebsocketEvents} from '@constants'; @@ -26,8 +26,6 @@ import TestHelper from 'test/test_helper'; import configureStore from 'test/test_store'; import initial_state from 'app/initial_state'; -require('isomorphic-fetch'); - global.WebSocket = MockWebSocket; const mockConfigRequest = (config = {}) => { diff --git a/app/actions/websocket.ts b/app/actions/websocket.ts index c0d815684..123d25711 100644 --- a/app/actions/websocket.ts +++ b/app/actions/websocket.ts @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import websocketClient from '@websocket'; -import {ChannelTypes, GeneralTypes, EmojiTypes, PostTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes, IntegrationTypes} from 'mattermost-redux/action_types'; -import {General, Preferences} from 'mattermost-redux/constants'; +import {ChannelTypes, GeneralTypes, EmojiTypes, PostTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes, IntegrationTypes} from '@mm-redux/action_types'; +import {General, Preferences} from '@mm-redux/constants'; import { getAllChannels, getChannel, @@ -17,26 +17,26 @@ import { getChannelMembersInChannels, isManuallyUnread, getKnownUsers, -} from 'mattermost-redux/selectors/entities/channels'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getAllPosts, getPost as selectPost} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentTeamId, getTeams as getTeamsSelector} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUser, getCurrentUserId, getUsers, getUserStatuses} from 'mattermost-redux/selectors/entities/users'; -import {getChannelByName, getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; -import {isGuest, removeUserFromList} from 'mattermost-redux/utils/user_utils'; -import {isFromWebhook, isSystemMessage, shouldIgnorePost} from 'mattermost-redux/utils/post_utils'; +} from '@mm-redux/selectors/entities/channels'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getAllPosts, getPost as selectPost} from '@mm-redux/selectors/entities/posts'; +import {getCurrentTeamId, getTeams as getTeamsSelector} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUser, getCurrentUserId, getUsers, getUserStatuses} from '@mm-redux/selectors/entities/users'; +import {getChannelByName, getUserIdFromChannelName} from '@mm-redux/utils/channel_utils'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {isGuest, removeUserFromList} from '@mm-redux/utils/user_utils'; +import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@mm-redux/utils/post_utils'; -import {DispatchFunc, GenericAction, GetStateFunc, batchActions} from 'mattermost-redux/types/actions'; +import {DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions'; -import {getCustomEmojiForReaction, getUnreadPostData, postDeleted, receivedNewPost, receivedPost} from 'mattermost-redux/actions/posts'; -import {markChannelAsRead} from 'mattermost-redux/actions/channels'; -import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users'; -import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'; -import {PreferenceType} from 'mattermost-redux/types/preferences'; -import {TeamMembership} from 'mattermost-redux/types/teams'; -import {Dictionary} from 'mattermost-redux/types/utilities'; +import {getCustomEmojiForReaction, getUnreadPostData, postDeleted, receivedNewPost, receivedPost} from '@mm-redux/actions/posts'; +import {markChannelAsRead} from '@mm-redux/actions/channels'; +import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users'; +import {Channel, ChannelMembership} from '@mm-redux/types/channels'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {TeamMembership} from '@mm-redux/types/teams'; +import {Dictionary} from '@mm-redux/types/utilities'; import {WebsocketEvents} from '@constants'; import { @@ -51,7 +51,7 @@ import { import {loadChannelsForTeam, markAsViewedAndReadBatch} from '@actions/views/channel'; import {getPost, getPosts, getPostsAdditionalDataBatch, getPostThread} from '@actions/views/post'; import {getMe, loadMe} from '@actions/views/user'; -import {GlobalState} from 'mattermost-redux/types/store'; +import {GlobalState} from '@mm-redux/types/store'; export type WebsocketBroadcast = { omit_users: Dictionary; diff --git a/app/components/announcement_banner/announcement_banner.test.js b/app/components/announcement_banner/announcement_banner.test.js index e332facac..d12d1a0c7 100644 --- a/app/components/announcement_banner/announcement_banner.test.js +++ b/app/components/announcement_banner/announcement_banner.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import AnnouncementBanner from './announcement_banner.js'; diff --git a/app/components/announcement_banner/index.js b/app/components/announcement_banner/index.js index 8033244d0..64d45b7a4 100644 --- a/app/components/announcement_banner/index.js +++ b/app/components/announcement_banner/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/components/at_mention/at_mention.js b/app/components/at_mention/at_mention.js index 9104f239a..dc1d54798 100644 --- a/app/components/at_mention/at_mention.js +++ b/app/components/at_mention/at_mention.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {Clipboard, Text} from 'react-native'; import {intlShape} from 'react-intl'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import CustomPropTypes from 'app/constants/custom_prop_types'; import mattermostManaged from 'app/mattermost_managed'; diff --git a/app/components/at_mention/index.js b/app/components/at_mention/index.js index 93345614d..e89d78624 100644 --- a/app/components/at_mention/index.js +++ b/app/components/at_mention/index.js @@ -3,9 +3,9 @@ import {connect} from 'react-redux'; -import {getUsersByUsername, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users'; +import {getUsersByUsername, getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import AtMention from './at_mention'; diff --git a/app/components/attachment_button/index.js b/app/components/attachment_button/index.js index 646ed3134..7f83f6bf0 100644 --- a/app/components/attachment_button/index.js +++ b/app/components/attachment_button/index.js @@ -19,7 +19,7 @@ import DocumentPicker from 'react-native-document-picker'; import ImagePicker from 'react-native-image-picker'; import Permissions from 'react-native-permissions'; -import {lookupMimeType} from 'mattermost-redux/utils/file_utils'; +import {lookupMimeType} from '@mm-redux/utils/file_utils'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import emmProvider from 'app/init/emm_provider'; diff --git a/app/components/attachment_button/index.test.js b/app/components/attachment_button/index.test.js index db9b136af..f55492a0b 100644 --- a/app/components/attachment_button/index.test.js +++ b/app/components/attachment_button/index.test.js @@ -7,7 +7,7 @@ import {shallow} from 'enzyme'; import Permissions from 'react-native-permissions'; import {Alert, StatusBar} from 'react-native'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile'; diff --git a/app/components/autocomplete/at_mention/at_mention.js b/app/components/autocomplete/at_mention/at_mention.js index c3b00754c..30e287bb3 100644 --- a/app/components/autocomplete/at_mention/at_mention.js +++ b/app/components/autocomplete/at_mention/at_mention.js @@ -5,7 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {SectionList} from 'react-native'; -import {RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from '@mm-redux/constants'; import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete'; import AtMentionItem from 'app/components/autocomplete/at_mention_item'; diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js index 011d23cf9..a8d2b5969 100644 --- a/app/components/autocomplete/at_mention/index.js +++ b/app/components/autocomplete/at_mention/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; -import {autocompleteUsers} from 'mattermost-redux/actions/users'; -import {getCurrentChannelId, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {autocompleteUsers} from '@mm-redux/actions/users'; +import {getCurrentChannelId, getDefaultChannel} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; import {isLandscape} from 'app/selectors/device'; import { @@ -16,10 +16,10 @@ import { filterMembersInCurrentTeam, getMatchTermForAtMention, } from 'app/selectors/autocomplete'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; -import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; -import {Permissions} from 'mattermost-redux/constants'; +import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles'; +import {Permissions} from '@mm-redux/constants'; import AtMention from './at_mention'; diff --git a/app/components/autocomplete/at_mention_item/index.js b/app/components/autocomplete/at_mention_item/index.js index 73a584a66..1a513be42 100644 --- a/app/components/autocomplete/at_mention_item/index.js +++ b/app/components/autocomplete/at_mention_item/index.js @@ -3,9 +3,9 @@ import {connect} from 'react-redux'; -import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import AtMentionItem from './at_mention_item'; diff --git a/app/components/autocomplete/autocomplete.js b/app/components/autocomplete/autocomplete.js index d0174ba52..30255b3d3 100644 --- a/app/components/autocomplete/autocomplete.js +++ b/app/components/autocomplete/autocomplete.js @@ -9,7 +9,7 @@ import { View, } from 'react-native'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {DeviceTypes} from 'app/constants'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/components/autocomplete/autocomplete_divider/index.js b/app/components/autocomplete/autocomplete_divider/index.js index ae104c41b..8e7799ef4 100644 --- a/app/components/autocomplete/autocomplete_divider/index.js +++ b/app/components/autocomplete/autocomplete_divider/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import AutocompleteDivider from './autocomplete_divider'; diff --git a/app/components/autocomplete/channel_mention/channel_mention.js b/app/components/autocomplete/channel_mention/channel_mention.js index 928b73ff1..f4249e877 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.js +++ b/app/components/autocomplete/channel_mention/channel_mention.js @@ -5,9 +5,9 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {Platform, SectionList} from 'react-native'; -import {RequestStatus} from 'mattermost-redux/constants'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; -import {debounce} from 'mattermost-redux/actions/helpers'; +import {RequestStatus} from '@mm-redux/constants'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {debounce} from '@mm-redux/actions/helpers'; import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete'; import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header'; diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js index 7bc6974b8..5c5087020 100644 --- a/app/components/autocomplete/channel_mention/index.js +++ b/app/components/autocomplete/channel_mention/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {searchChannels, autocompleteChannelsForSearch} from 'mattermost-redux/actions/channels'; -import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels'; +import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; import {isLandscape} from 'app/selectors/device'; import { @@ -17,7 +17,7 @@ import { filterDirectAndGroupMessages, getMatchTermForChannelMention, } from 'app/selectors/autocomplete'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import ChannelMention from './channel_mention'; diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.js b/app/components/autocomplete/channel_mention_item/channel_mention_item.js index 599249a43..4619f5ff4 100644 --- a/app/components/autocomplete/channel_mention_item/channel_mention_item.js +++ b/app/components/autocomplete/channel_mention_item/channel_mention_item.js @@ -7,7 +7,7 @@ import { Text, } from 'react-native'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider'; import {BotTag, GuestTag} from 'app/components/tag'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; diff --git a/app/components/autocomplete/channel_mention_item/index.js b/app/components/autocomplete/channel_mention_item/index.js index 1671fd391..0a97e4398 100644 --- a/app/components/autocomplete/channel_mention_item/index.js +++ b/app/components/autocomplete/channel_mention_item/index.js @@ -3,10 +3,10 @@ import {connect} from 'react-redux'; -import {General} from 'mattermost-redux/constants'; -import {getChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getUser} from 'mattermost-redux/selectors/entities/users'; +import {General} from '@mm-redux/constants'; +import {getChannel} from '@mm-redux/selectors/entities/channels'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getUser} from '@mm-redux/selectors/entities/users'; import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/components/autocomplete/date_suggestion/date_suggestion.js b/app/components/autocomplete/date_suggestion/date_suggestion.js index 27cac7a56..ff900811c 100644 --- a/app/components/autocomplete/date_suggestion/date_suggestion.js +++ b/app/components/autocomplete/date_suggestion/date_suggestion.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import {CalendarList, LocaleConfig} from 'react-native-calendars'; import {intlShape} from 'react-intl'; -import {memoizeResult} from 'mattermost-redux/utils/helpers'; +import {memoizeResult} from '@mm-redux/utils/helpers'; import {DATE_MENTION_SEARCH_REGEX, ALL_SEARCH_FLAGS_REGEX} from 'app/constants/autocomplete'; import {changeOpacity} from 'app/utils/theme'; diff --git a/app/components/autocomplete/date_suggestion/index.js b/app/components/autocomplete/date_suggestion/index.js index f44655074..54674ee87 100644 --- a/app/components/autocomplete/date_suggestion/index.js +++ b/app/components/autocomplete/date_suggestion/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {makeGetMatchTermForDateMention} from 'app/selectors/autocomplete'; import {getCurrentLocale} from 'app/selectors/i18n'; diff --git a/app/components/autocomplete/emoji_suggestion/index.js b/app/components/autocomplete/emoji_suggestion/index.js index f54c2df34..455ac8af1 100644 --- a/app/components/autocomplete/emoji_suggestion/index.js +++ b/app/components/autocomplete/emoji_suggestion/index.js @@ -5,11 +5,11 @@ import {connect} from 'react-redux'; import {createSelector} from 'reselect'; import {bindActionCreators} from 'redux'; -import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; -import {autocompleteCustomEmojis} from 'mattermost-redux/actions/emojis'; +import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis'; +import {autocompleteCustomEmojis} from '@mm-redux/actions/emojis'; import {addReactionToLatestPost} from 'app/actions/views/emoji'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {EmojiIndicesByAlias} from 'app/utils/emojis'; import EmojiSuggestion from './emoji_suggestion'; diff --git a/app/components/autocomplete/index.js b/app/components/autocomplete/index.js index 0305722f6..31c6b8d24 100644 --- a/app/components/autocomplete/index.js +++ b/app/components/autocomplete/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/components/autocomplete/slash_suggestion/index.js b/app/components/autocomplete/slash_suggestion/index.js index ecf5e06df..a05e54526 100644 --- a/app/components/autocomplete/slash_suggestion/index.js +++ b/app/components/autocomplete/slash_suggestion/index.js @@ -5,10 +5,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {createSelector} from 'reselect'; -import {getAutocompleteCommands} from 'mattermost-redux/actions/integrations'; -import {getAutocompleteCommandsList} from 'mattermost-redux/selectors/entities/integrations'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getAutocompleteCommands} from '@mm-redux/actions/integrations'; +import {getAutocompleteCommandsList} from '@mm-redux/selectors/entities/integrations'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; import {isLandscape} from 'app/selectors/device'; import SlashSuggestion from './slash_suggestion'; diff --git a/app/components/autocomplete_selector/autocomplete_selector.js b/app/components/autocomplete_selector/autocomplete_selector.js index c5b8c1499..77c3e8723 100644 --- a/app/components/autocomplete_selector/autocomplete_selector.js +++ b/app/components/autocomplete_selector/autocomplete_selector.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/FontAwesome'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import FormattedText from 'app/components/formatted_text'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; diff --git a/app/components/autocomplete_selector/index.js b/app/components/autocomplete_selector/index.js index 3a0c42d46..67a148292 100644 --- a/app/components/autocomplete_selector/index.js +++ b/app/components/autocomplete_selector/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import {setAutocompleteSelector} from 'app/actions/views/post'; diff --git a/app/components/channel_icon.js b/app/components/channel_icon.js index 9e2db77b5..6a7931a53 100644 --- a/app/components/channel_icon.js +++ b/app/components/channel_icon.js @@ -8,7 +8,7 @@ import { View, } from 'react-native'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import Icon from 'app/components/vector_icon'; diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index 92290424a..a3729bc90 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -9,8 +9,8 @@ import { } from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; -import {General} from 'mattermost-redux/constants'; +import {displayUsername} from '@mm-redux/utils/user_utils'; +import {General} from '@mm-redux/constants'; import {goToScreen} from 'app/actions/navigation'; import ProfilePicture from 'app/components/profile_picture'; diff --git a/app/components/channel_intro/index.js b/app/components/channel_intro/index.js index 8a58057c9..b46acabb8 100644 --- a/app/components/channel_intro/index.js +++ b/app/components/channel_intro/index.js @@ -4,11 +4,11 @@ import {connect} from 'react-redux'; import {createSelector} from 'reselect'; -import {General} from 'mattermost-redux/constants'; -import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users'; +import {General} from '@mm-redux/constants'; +import {makeGetChannel} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId, getUser, makeGetProfilesInChannel} from '@mm-redux/selectors/entities/users'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import {getChannelMembersForDm} from 'app/selectors/channel'; diff --git a/app/components/channel_link/index.js b/app/components/channel_link/index.js index 46c6734ab..195cb1781 100644 --- a/app/components/channel_link/index.js +++ b/app/components/channel_link/index.js @@ -5,10 +5,10 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {createSelector} from 'reselect'; -import {joinChannel} from 'mattermost-redux/actions/channels'; -import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {joinChannel} from '@mm-redux/actions/channels'; +import {getChannelsNameMapInCurrentTeam} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; import {handleSelectChannel} from 'app/actions/views/channel'; diff --git a/app/components/channel_loader/channel_loader.js b/app/components/channel_loader/channel_loader.js index 1fb84f90d..642e9ac3e 100644 --- a/app/components/channel_loader/channel_loader.js +++ b/app/components/channel_loader/channel_loader.js @@ -11,7 +11,7 @@ import { } from 'react-native'; import {ImageContent} from 'rn-placeholder'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import CustomPropTypes from 'app/constants/custom_prop_types'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/components/channel_loader/channel_loader.test.js b/app/components/channel_loader/channel_loader.test.js index 5ae88aaa7..df58d8773 100644 --- a/app/components/channel_loader/channel_loader.test.js +++ b/app/components/channel_loader/channel_loader.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ChannelLoader from './channel_loader'; diff --git a/app/components/channel_loader/index.js b/app/components/channel_loader/index.js index aa7d9b53f..acde71df6 100644 --- a/app/components/channel_loader/index.js +++ b/app/components/channel_loader/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel'; diff --git a/app/components/client_upgrade_listener/index.js b/app/components/client_upgrade_listener/index.js index 8d4483fc8..49af5bd47 100644 --- a/app/components/client_upgrade_listener/index.js +++ b/app/components/client_upgrade_listener/index.js @@ -3,8 +3,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {logError} from 'mattermost-redux/actions/errors'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {logError} from '@mm-redux/actions/errors'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import getClientUpgrade from 'app/selectors/client_upgrade'; diff --git a/app/components/combined_system_message/combined_system_message.js b/app/components/combined_system_message/combined_system_message.js index fd66ff9b7..ec71eb394 100644 --- a/app/components/combined_system_message/combined_system_message.js +++ b/app/components/combined_system_message/combined_system_message.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {intlShape} from 'react-intl'; -import {Posts} from 'mattermost-redux/constants'; +import {Posts} from '@mm-redux/constants'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import {t} from 'app/utils/i18n'; diff --git a/app/components/combined_system_message/index.js b/app/components/combined_system_message/index.js index 70b14c446..7ec132f5a 100644 --- a/app/components/combined_system_message/index.js +++ b/app/components/combined_system_message/index.js @@ -4,10 +4,10 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getMissingProfilesByIds, getMissingProfilesByUsernames} from 'mattermost-redux/actions/users'; -import {Preferences} from 'mattermost-redux/constants'; -import {getBool} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUser, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users'; +import {getMissingProfilesByIds, getMissingProfilesByUsernames} from '@mm-redux/actions/users'; +import {Preferences} from '@mm-redux/constants'; +import {getBool} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUser, makeGetProfilesByIdsAndUsernames} from '@mm-redux/selectors/entities/users'; import CombinedSystemMessage from './combined_system_message'; diff --git a/app/components/combined_system_message/last_users.js b/app/components/combined_system_message/last_users.js index f85600ff7..899e82ec5 100644 --- a/app/components/combined_system_message/last_users.js +++ b/app/components/combined_system_message/last_users.js @@ -6,7 +6,7 @@ import React from 'react'; import {Text} from 'react-native'; import {intlShape} from 'react-intl'; -import {Posts} from 'mattermost-redux/constants'; +import {Posts} from '@mm-redux/constants'; import FormattedMarkdownText from 'app/components/formatted_markdown_text'; import FormattedText from 'app/components/formatted_text'; diff --git a/app/components/combined_user_activity_post/index.js b/app/components/combined_user_activity_post/index.js index 2b2b97e77..f4684a52a 100644 --- a/app/components/combined_user_activity_post/index.js +++ b/app/components/combined_user_activity_post/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {makeGenerateCombinedPost} from 'mattermost-redux/utils/post_list'; +import {makeGenerateCombinedPost} from '@mm-redux/utils/post_list'; import Post from 'app/components/post'; diff --git a/app/components/custom_list/channel_list_row/index.js b/app/components/custom_list/channel_list_row/index.js index 113da9f39..948a182f3 100644 --- a/app/components/custom_list/channel_list_row/index.js +++ b/app/components/custom_list/channel_list_row/index.js @@ -3,9 +3,9 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; -import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; +import {makeGetChannel} from '@mm-redux/selectors/entities/channels'; import {isLandscape} from 'app/selectors/device'; import ChannelListRow from './channel_list_row'; diff --git a/app/components/custom_list/index.test.js b/app/components/custom_list/index.test.js index b1b34c48d..68e6477d2 100644 --- a/app/components/custom_list/index.test.js +++ b/app/components/custom_list/index.test.js @@ -5,7 +5,7 @@ import React from 'react'; import {View} from 'react-native'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import CustomList, {FLATLIST, SECTIONLIST} from './index'; diff --git a/app/components/custom_list/option_list_row/index.js b/app/components/custom_list/option_list_row/index.js index 83aa84821..8b3d5af46 100644 --- a/app/components/custom_list/option_list_row/index.js +++ b/app/components/custom_list/option_list_row/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import OptionListRow from './option_list_row'; diff --git a/app/components/custom_list/user_list_row/index.js b/app/components/custom_list/user_list_row/index.js index 8de48fbcf..50dbab1a4 100644 --- a/app/components/custom_list/user_list_row/index.js +++ b/app/components/custom_list/user_list_row/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; import {isLandscape} from 'app/selectors/device'; import UserListRow from './user_list_row'; diff --git a/app/components/custom_list/user_list_row/user_list_row.js b/app/components/custom_list/user_list_row/user_list_row.js index 4e8ebf708..1b620b6ca 100644 --- a/app/components/custom_list/user_list_row/user_list_row.js +++ b/app/components/custom_list/user_list_row/user_list_row.js @@ -9,7 +9,7 @@ import { View, } from 'react-native'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import CustomListRow from 'app/components/custom_list/custom_list_row'; import ProfilePicture from 'app/components/profile_picture'; diff --git a/app/components/custom_list/user_list_row/user_list_test.test.js b/app/components/custom_list/user_list_row/user_list_test.test.js index d2e62bcb1..d0403b30d 100644 --- a/app/components/custom_list/user_list_row/user_list_test.test.js +++ b/app/components/custom_list/user_list_row/user_list_test.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import UserListRow from './user_list_row'; diff --git a/app/components/edit_channel_info/edit_channel_info.js b/app/components/edit_channel_info/edit_channel_info.js index 346f13692..7a6796812 100644 --- a/app/components/edit_channel_info/edit_channel_info.js +++ b/app/components/edit_channel_info/edit_channel_info.js @@ -10,7 +10,7 @@ import { } from 'react-native'; import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import Autocomplete from 'app/components/autocomplete'; import ErrorText from 'app/components/error_text'; diff --git a/app/components/edit_channel_info/edit_channel_info.test.js b/app/components/edit_channel_info/edit_channel_info.test.js index e4b9b303a..5e1f81b94 100644 --- a/app/components/edit_channel_info/edit_channel_info.test.js +++ b/app/components/edit_channel_info/edit_channel_info.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import Autocomplete from 'app/components/autocomplete'; import EditChannelInfo from './edit_channel_info'; diff --git a/app/components/emoji/index.js b/app/components/emoji/index.js index 3e6dc2334..306d7262d 100644 --- a/app/components/emoji/index.js +++ b/app/components/emoji/index.js @@ -3,11 +3,11 @@ import {connect} from 'react-redux'; -import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {Client4} from 'mattermost-redux/client'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {Client4} from '@mm-redux/client'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis'; diff --git a/app/components/emoji_picker/emoji_picker.test.js b/app/components/emoji_picker/emoji_picker.test.js index 2b0b24d8b..2cf800492 100644 --- a/app/components/emoji_picker/emoji_picker.test.js +++ b/app/components/emoji_picker/emoji_picker.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; import {filterEmojiSearchInput} from './emoji_picker_base'; diff --git a/app/components/emoji_picker/index.js b/app/components/emoji_picker/index.js index a6b34ca07..6744b9d59 100644 --- a/app/components/emoji_picker/index.js +++ b/app/components/emoji_picker/index.js @@ -5,10 +5,10 @@ import {connect} from 'react-redux'; import {createSelector} from 'reselect'; import {bindActionCreators} from 'redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getCustomEmojis, searchCustomEmojis} from 'mattermost-redux/actions/emojis'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getCustomEmojis, searchCustomEmojis} from '@mm-redux/actions/emojis'; import {incrementEmojiPickerPage} from 'app/actions/views/emoji'; import {getDimensions, isLandscape} from 'app/selectors/device'; diff --git a/app/components/error_list/index.js b/app/components/error_list/index.js index 38c54ec3c..74a0f23c4 100644 --- a/app/components/error_list/index.js +++ b/app/components/error_list/index.js @@ -4,8 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getDisplayableErrors} from 'mattermost-redux/selectors/errors'; -import {dismissError, clearErrors} from 'mattermost-redux/actions/errors'; +import {getDisplayableErrors} from '@mm-redux/selectors/errors'; +import {dismissError, clearErrors} from '@mm-redux/actions/errors'; import ErrorList from './error_list'; diff --git a/app/components/error_text/error_text.test.js b/app/components/error_text/error_text.test.js index 6c85f56b8..ddcb88fcf 100644 --- a/app/components/error_text/error_text.test.js +++ b/app/components/error_text/error_text.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ErrorText from './error_text.js'; diff --git a/app/components/error_text/index.js b/app/components/error_text/index.js index ba5888b96..82379ce23 100644 --- a/app/components/error_text/index.js +++ b/app/components/error_text/index.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import ErrorText from './error_text.js'; diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js index 7a50c637d..f4be128ab 100644 --- a/app/components/file_attachment_list/file_attachment.js +++ b/app/components/file_attachment_list/file_attachment.js @@ -11,7 +11,7 @@ import { StyleSheet, } from 'react-native'; -import * as Utils from 'mattermost-redux/utils/file_utils.js'; +import * as Utils from '@mm-redux/utils/file_utils'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import {isDocument, isGif} from 'app/utils/file'; diff --git a/app/components/file_attachment_list/file_attachment.test.js b/app/components/file_attachment_list/file_attachment.test.js index 7f0765d49..4c289ef5e 100644 --- a/app/components/file_attachment_list/file_attachment.test.js +++ b/app/components/file_attachment_list/file_attachment.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import FileAttachment from './file_attachment.js'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; jest.mock('react-native-doc-viewer', () => ({ openDoc: jest.fn(), diff --git a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js index 67bb75aa2..7173b00c2 100644 --- a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js @@ -19,7 +19,7 @@ import {CircularProgress} from 'react-native-circular-progress'; import {intlShape} from 'react-intl'; import tinyColor from 'tinycolor2'; -import {getFileUrl} from 'mattermost-redux/utils/file_utils.js'; +import {getFileUrl} from '@mm-redux/utils/file_utils'; import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; diff --git a/app/components/file_attachment_list/file_attachment_icon.js b/app/components/file_attachment_list/file_attachment_icon.js index 08f6ba039..8b9c2db4d 100644 --- a/app/components/file_attachment_list/file_attachment_icon.js +++ b/app/components/file_attachment_list/file_attachment_icon.js @@ -9,7 +9,7 @@ import { StyleSheet, } from 'react-native'; -import * as Utils from 'mattermost-redux/utils/file_utils'; +import * as Utils from '@mm-redux/utils/file_utils'; import audioIcon from 'assets/images/icons/audio.png'; import codeIcon from 'assets/images/icons/code.png'; diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js index 1dc326ed6..82b8bd214 100644 --- a/app/components/file_attachment_list/file_attachment_image.js +++ b/app/components/file_attachment_list/file_attachment_image.js @@ -10,7 +10,7 @@ import { } from 'react-native'; import FastImage from 'react-native-fast-image'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import ProgressiveImage from 'app/components/progressive_image'; import {changeOpacity} from 'app/utils/theme'; diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index 726c680cb..dcb70674c 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -6,8 +6,8 @@ import PropTypes from 'prop-types'; import {Dimensions, StyleSheet, View} from 'react-native'; import AsyncStorage from '@react-native-community/async-storage'; -import {Client4} from 'mattermost-redux/client'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {Client4} from '@mm-redux/client'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import {DeviceTypes} from 'app/constants'; diff --git a/app/components/file_attachment_list/file_attachment_list.test.js b/app/components/file_attachment_list/file_attachment_list.test.js index 21d97fe46..ddad81fb2 100644 --- a/app/components/file_attachment_list/file_attachment_list.test.js +++ b/app/components/file_attachment_list/file_attachment_list.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import FileAttachment from './file_attachment_list.js'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; jest.mock('react-native-doc-viewer', () => ({ openDoc: jest.fn(), diff --git a/app/components/file_attachment_list/index.js b/app/components/file_attachment_list/index.js index 26bc64c1e..ca8025f10 100644 --- a/app/components/file_attachment_list/index.js +++ b/app/components/file_attachment_list/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/general'; -import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {canDownloadFilesOnMobile} from '@mm-redux/selectors/entities/general'; +import {makeGetFilesForPost} from '@mm-redux/selectors/entities/files'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {loadFilesForPostIfNecessary} from 'app/actions/views/channel'; diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.js index 19d9b4613..d957cb818 100644 --- a/app/components/file_upload_preview/file_upload_item/file_upload_item.js +++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.js @@ -7,7 +7,7 @@ import {Text, View} from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; import {AnimatedCircularProgress} from 'react-native-circular-progress'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image'; import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.test.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.test.js index eca4744f3..0df6e7231 100644 --- a/app/components/file_upload_preview/file_upload_item/file_upload_item.test.js +++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import ImageCacheManager from 'app/utils/image_cache_manager'; import FileUploadItem from './file_upload_item'; diff --git a/app/components/file_upload_preview/file_upload_item/index.js b/app/components/file_upload_preview/file_upload_item/index.js index 1553c6ca8..280fc9cb3 100644 --- a/app/components/file_upload_preview/file_upload_item/index.js +++ b/app/components/file_upload_preview/file_upload_item/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {handleRemoveFile, retryFileUpload, uploadComplete, uploadFailed} from 'app/actions/views/file_upload'; diff --git a/app/components/file_upload_preview/file_upload_preview.js b/app/components/file_upload_preview/file_upload_preview.js index b114e9057..8d588b511 100644 --- a/app/components/file_upload_preview/file_upload_preview.js +++ b/app/components/file_upload_preview/file_upload_preview.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import * as Animatable from 'react-native-animatable'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import FormattedText from 'app/components/formatted_text'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/components/file_upload_preview/index.js b/app/components/file_upload_preview/index.js index 15248cd91..1124bce27 100644 --- a/app/components/file_upload_preview/index.js +++ b/app/components/file_upload_preview/index.js @@ -2,8 +2,8 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getDimensions} from 'app/selectors/device'; import {checkForFileUploadingInChannel} from 'app/selectors/file'; diff --git a/app/components/interactive_dialog_controller/index.js b/app/components/interactive_dialog_controller/index.js index 0c23b8d31..590ecc2cd 100644 --- a/app/components/interactive_dialog_controller/index.js +++ b/app/components/interactive_dialog_controller/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations'; +import {submitInteractiveDialog} from '@mm-redux/actions/integrations'; import InteractiveDialogController from './interactive_dialog_controller'; diff --git a/app/components/markdown/index.js b/app/components/markdown/index.js index c4e90aee9..2e484bf2a 100644 --- a/app/components/markdown/index.js +++ b/app/components/markdown/index.js @@ -3,9 +3,9 @@ import {connect} from 'react-redux'; -import {getAutolinkedUrlSchemes, getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users'; +import {getAutolinkedUrlSchemes, getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users'; import Markdown from './markdown'; diff --git a/app/components/markdown/markdown_code_block/index.js b/app/components/markdown/markdown_code_block/index.js index 37ccabce4..1c06f354d 100644 --- a/app/components/markdown/markdown_code_block/index.js +++ b/app/components/markdown/markdown_code_block/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import MarkdownCodeBlock from './markdown_code_block'; diff --git a/app/components/markdown/markdown_emoji/index.js b/app/components/markdown/markdown_emoji/index.js index 11c2e2cf9..3869e7808 100644 --- a/app/components/markdown/markdown_emoji/index.js +++ b/app/components/markdown/markdown_emoji/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import MarkdownEmoji from './markdown_emoji'; diff --git a/app/components/markdown/markdown_emoji/markdown_emoji.test.js b/app/components/markdown/markdown_emoji/markdown_emoji.test.js index cd310fe61..6953bfe1f 100644 --- a/app/components/markdown/markdown_emoji/markdown_emoji.test.js +++ b/app/components/markdown/markdown_emoji/markdown_emoji.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import MarkdownEmoji from './markdown_emoji'; diff --git a/app/components/markdown/markdown_image/index.js b/app/components/markdown/markdown_image/index.js index 19ef387fa..41dc8e0e0 100644 --- a/app/components/markdown/markdown_image/index.js +++ b/app/components/markdown/markdown_image/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; +import {getCurrentUrl} from '@mm-redux/selectors/entities/general'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/components/markdown/markdown_link/index.js b/app/components/markdown/markdown_link/index.js index b280f3645..e30878f02 100644 --- a/app/components/markdown/markdown_link/index.js +++ b/app/components/markdown/markdown_link/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; +import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general'; import {handleSelectChannelByName} from 'app/actions/views/channel'; import MarkdownLink from './markdown_link'; diff --git a/app/components/markdown/markdown_table/index.js b/app/components/markdown/markdown_table/index.js index e3e37c016..805cfd82d 100644 --- a/app/components/markdown/markdown_table/index.js +++ b/app/components/markdown/markdown_table/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import MarkdownTable from './markdown_table'; diff --git a/app/components/markdown/markdown_table/markdown_table.test.js b/app/components/markdown/markdown_table/markdown_table.test.js index 6406c8266..d17bbeca5 100644 --- a/app/components/markdown/markdown_table/markdown_table.test.js +++ b/app/components/markdown/markdown_table/markdown_table.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallowWithIntl} from 'test/intl-test-helper'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import MarkdownTable from './markdown_table'; diff --git a/app/components/markdown/markdown_table_cell/index.js b/app/components/markdown/markdown_table_cell/index.js index 450c07056..0a8733d2c 100644 --- a/app/components/markdown/markdown_table_cell/index.js +++ b/app/components/markdown/markdown_table_cell/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import MarkdownTableCell from './markdown_table_cell'; diff --git a/app/components/markdown/markdown_table_image/index.js b/app/components/markdown/markdown_table_image/index.js index 96174a3a8..ed4e50be7 100644 --- a/app/components/markdown/markdown_table_image/index.js +++ b/app/components/markdown/markdown_table_image/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentUrl} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import MarkdownTableImage from './markdown_table_image'; diff --git a/app/components/markdown/markdown_table_row/index.js b/app/components/markdown/markdown_table_row/index.js index ce6b80d3b..33fbd3392 100644 --- a/app/components/markdown/markdown_table_row/index.js +++ b/app/components/markdown/markdown_table_row/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import MarkdownTableRow from './markdown_table_row'; diff --git a/app/components/message_attachments/action_button/action_button.test.js b/app/components/message_attachments/action_button/action_button.test.js index c52ceefac..f9a8c5cac 100644 --- a/app/components/message_attachments/action_button/action_button.test.js +++ b/app/components/message_attachments/action_button/action_button.test.js @@ -7,7 +7,7 @@ import ActionButton from './action_button'; import {changeOpacity} from 'app/utils/theme'; import {getStatusColors} from 'app/utils/message_attachment_colors'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; describe('ActionButton', () => { test('correct styles when from global theme', () => { diff --git a/app/components/message_attachments/action_button/index.js b/app/components/message_attachments/action_button/index.js index fc7e7cec3..818d43ca0 100644 --- a/app/components/message_attachments/action_button/index.js +++ b/app/components/message_attachments/action_button/index.js @@ -4,8 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {doPostActionWithCookie} from 'mattermost-redux/actions/posts'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {doPostActionWithCookie} from '@mm-redux/actions/posts'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import ActionButton from './action_button'; diff --git a/app/components/message_attachments/attachment_footer.test.js b/app/components/message_attachments/attachment_footer.test.js index 7bf46be1b..1bbf8cd9e 100644 --- a/app/components/message_attachments/attachment_footer.test.js +++ b/app/components/message_attachments/attachment_footer.test.js @@ -7,7 +7,7 @@ import {shallow} from 'enzyme'; import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment'; import AttachmentFooter from './attachment_footer'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; describe('AttachmentFooter', () => { const baseProps = { diff --git a/app/components/message_attachments/attachment_image/index.test.js b/app/components/message_attachments/attachment_image/index.test.js index 27c16f9f9..1ffb4c7f5 100644 --- a/app/components/message_attachments/attachment_image/index.test.js +++ b/app/components/message_attachments/attachment_image/index.test.js @@ -7,7 +7,7 @@ import React from 'react'; const originalGetSizeFn = Image.getSize; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import AttachmentImage from './index'; diff --git a/app/components/network_indicator/index.js b/app/components/network_indicator/index.js index a0455efcb..d9ac6afbb 100644 --- a/app/components/network_indicator/index.js +++ b/app/components/network_indicator/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {stopPeriodicStatusUpdates, startPeriodicStatusUpdates} from 'mattermost-redux/actions/users'; +import {stopPeriodicStatusUpdates, startPeriodicStatusUpdates} from '@mm-redux/actions/users'; import {init as initWebSocket, close as closeWebSocket} from '@actions/websocket'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; import {connection} from 'app/actions/device'; import {markChannelViewedAndReadOnReconnect, setChannelRetryFailed} from 'app/actions/views/channel'; diff --git a/app/components/network_indicator/network_indicator.js b/app/components/network_indicator/network_indicator.js index 757ac2691..d75160a43 100644 --- a/app/components/network_indicator/network_indicator.js +++ b/app/components/network_indicator/network_indicator.js @@ -23,7 +23,7 @@ import PushNotifications from 'app/push_notifications'; import networkConnectionListener, {checkConnection} from 'app/utils/network'; import {t} from 'app/utils/i18n'; -import {RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from '@mm-redux/constants'; const HEIGHT = 38; const MAX_WEBSOCKET_RETRIES = 3; diff --git a/app/components/post/index.js b/app/components/post/index.js index ef208d651..bd0fc40c5 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -4,14 +4,14 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {createPost, removePost} from 'mattermost-redux/actions/posts'; -import {Posts} from 'mattermost-redux/constants'; -import {isChannelReadOnlyById} from 'mattermost-redux/selectors/entities/channels'; -import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention} from 'mattermost-redux/selectors/entities/posts'; -import {getUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isStartOfNewMessages} from 'mattermost-redux/utils/post_list'; -import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils'; +import {createPost, removePost} from '@mm-redux/actions/posts'; +import {Posts} from '@mm-redux/constants'; +import {isChannelReadOnlyById} from '@mm-redux/selectors/entities/channels'; +import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention} from '@mm-redux/selectors/entities/posts'; +import {getUser, getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getMyPreferences, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isStartOfNewMessages} from '@mm-redux/utils/post_list'; +import {isPostFlagged, isSystemMessage} from '@mm-redux/utils/post_utils'; import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/components/post/post.js b/app/components/post/post.js index 769526775..20baa4bb3 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -11,9 +11,9 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; -import {Posts} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils'; +import {Posts} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from '@mm-redux/utils/post_utils'; import PostBody from 'app/components/post_body'; import PostHeader from 'app/components/post_header'; diff --git a/app/components/post_add_channel_member/index.js b/app/components/post_add_channel_member/index.js index 49b14c23a..f210e66ce 100644 --- a/app/components/post_add_channel_member/index.js +++ b/app/components/post_add_channel_member/index.js @@ -4,12 +4,12 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {addChannelMember} from 'mattermost-redux/actions/channels'; -import {removePost} from 'mattermost-redux/actions/posts'; +import {addChannelMember} from '@mm-redux/actions/channels'; +import {removePost} from '@mm-redux/actions/posts'; -import {getPost} from 'mattermost-redux/selectors/entities/posts'; -import {getChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getPost} from '@mm-redux/selectors/entities/posts'; +import {getChannel} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {sendAddToChannelEphemeralPost} from 'app/actions/views/post'; diff --git a/app/components/post_add_channel_member/post_add_channel_member.js b/app/components/post_add_channel_member/post_add_channel_member.js index e7e3992a2..1e2cbd403 100644 --- a/app/components/post_add_channel_member/post_add_channel_member.js +++ b/app/components/post_add_channel_member/post_add_channel_member.js @@ -7,7 +7,7 @@ import {intlShape} from 'react-intl'; import {Text} from 'react-native'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import {concatStyles} from 'app/utils/theme'; import {t} from 'app/utils/i18n'; diff --git a/app/components/post_attachment_opengraph/index.js b/app/components/post_attachment_opengraph/index.js index f422ddf5e..4214c917e 100644 --- a/app/components/post_attachment_opengraph/index.js +++ b/app/components/post_attachment_opengraph/index.js @@ -4,7 +4,7 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts'; +import {getOpenGraphMetadata} from '@mm-redux/actions/posts'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js index 699fa915e..628990814 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js @@ -6,7 +6,7 @@ import {shallow} from 'enzyme'; import {Image} from 'react-native'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import PostAttachmentOpenGraph from './post_attachment_opengraph'; diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js index 77e7d0c07..8d7cb1fdb 100644 --- a/app/components/post_body/index.js +++ b/app/components/post_body/index.js @@ -3,23 +3,23 @@ import {connect} from 'react-redux'; -import {General, Posts} from 'mattermost-redux/constants'; -import {getChannel, canManageChannelMembers, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentUserId, getCurrentUserRoles, getUser} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; -import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; -import {memoizeResult} from 'mattermost-redux/utils/helpers'; +import {General, Posts} from '@mm-redux/constants'; +import {getChannel, canManageChannelMembers, getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; +import {getCurrentUserId, getCurrentUserRoles, getUser} from '@mm-redux/selectors/entities/users'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis'; +import {makeGetReactionsForPost} from '@mm-redux/selectors/entities/posts'; +import {memoizeResult} from '@mm-redux/utils/helpers'; import { isEdited, isPostEphemeral, isSystemMessage, canDeletePost, -} from 'mattermost-redux/utils/post_utils'; -import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; +} from '@mm-redux/utils/post_utils'; +import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/components/post_body/index.test.js b/app/components/post_body/index.test.js index dd329fe0f..15a32c5f6 100644 --- a/app/components/post_body/index.test.js +++ b/app/components/post_body/index.test.js @@ -1,13 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getChannel} from 'mattermost-redux/selectors/entities/channels'; -import * as PostUtils from 'mattermost-redux/utils/post_utils'; +import {getChannel} from '@mm-redux/selectors/entities/channels'; +import * as PostUtils from '@mm-redux/utils/post_utils'; import {makeMapStateToProps} from './index.js'; -jest.mock('mattermost-redux/selectors/entities/channels', () => { - const channels = require.requireActual('mattermost-redux/selectors/entities/channels'); +jest.mock('@mm-redux/selectors/entities/channels', () => { + const channels = require.requireActual('@mm-redux/selectors/entities/channels'); return { ...channels, @@ -17,16 +17,16 @@ jest.mock('mattermost-redux/selectors/entities/channels', () => { }; }); -jest.mock('mattermost-redux/selectors/entities/preferences', () => { - const preferences = require.requireActual('mattermost-redux/selectors/entities/preferences'); +jest.mock('@mm-redux/selectors/entities/preferences', () => { + const preferences = require.requireActual('@mm-redux/selectors/entities/preferences'); return { ...preferences, getTheme: jest.fn(), }; }); -jest.mock('mattermost-redux/selectors/entities/general', () => { - const general = require.requireActual('mattermost-redux/selectors/entities/general'); +jest.mock('@mm-redux/selectors/entities/general', () => { + const general = require.requireActual('@mm-redux/selectors/entities/general'); return { ...general, getConfig: jest.fn(), @@ -34,8 +34,8 @@ jest.mock('mattermost-redux/selectors/entities/general', () => { }; }); -jest.mock('mattermost-redux/selectors/entities/users', () => { - const users = require.requireActual('mattermost-redux/selectors/entities/users'); +jest.mock('@mm-redux/selectors/entities/users', () => { + const users = require.requireActual('@mm-redux/selectors/entities/users'); return { ...users, getCurrentUserId: jest.fn(), @@ -43,24 +43,24 @@ jest.mock('mattermost-redux/selectors/entities/users', () => { }; }); -jest.mock('mattermost-redux/selectors/entities/teams', () => { - const teams = require.requireActual('mattermost-redux/selectors/entities/teams'); +jest.mock('@mm-redux/selectors/entities/teams', () => { + const teams = require.requireActual('@mm-redux/selectors/entities/teams'); return { ...teams, getCurrentTeamId: jest.fn(), }; }); -jest.mock('mattermost-redux/selectors/entities/emojis', () => { - const emojis = require.requireActual('mattermost-redux/selectors/entities/emojis'); +jest.mock('@mm-redux/selectors/entities/emojis', () => { + const emojis = require.requireActual('@mm-redux/selectors/entities/emojis'); return { ...emojis, getCustomEmojisByName: jest.fn(), }; }); -jest.mock('mattermost-redux/selectors/entities/posts', () => { - const posts = require.requireActual('mattermost-redux/selectors/entities/posts'); +jest.mock('@mm-redux/selectors/entities/posts', () => { + const posts = require.requireActual('@mm-redux/selectors/entities/posts'); return { ...posts, makeGetReactionsForPost: () => jest.fn(), diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 3dc7b00c2..9aebb7746 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/Ionicons'; -import {Posts} from 'mattermost-redux/constants'; +import {Posts} from '@mm-redux/constants'; import CombinedSystemMessage from 'app/components/combined_system_message'; import {renderSystemMessage} from './system_message_helpers'; diff --git a/app/components/post_body/post_body.test.js b/app/components/post_body/post_body.test.js index ac62b6db6..b453e4a1b 100644 --- a/app/components/post_body/post_body.test.js +++ b/app/components/post_body/post_body.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import PostBodyAdditionalContent from 'app/components/post_body_additional_content'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/components/post_body/system_message_helpers.js b/app/components/post_body/system_message_helpers.js index b5676fb0f..98594be21 100644 --- a/app/components/post_body/system_message_helpers.js +++ b/app/components/post_body/system_message_helpers.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; -import {Posts} from 'mattermost-redux/constants'; +import {Posts} from '@mm-redux/constants'; import Markdown from 'app/components/markdown'; import {t} from 'app/utils/i18n'; diff --git a/app/components/post_body/system_message_helpers.test.js b/app/components/post_body/system_message_helpers.test.js index dae0eaa97..b81d0496a 100644 --- a/app/components/post_body/system_message_helpers.test.js +++ b/app/components/post_body/system_message_helpers.test.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import * as SystemMessageHelpers from './system_message_helpers'; -import {Posts} from 'mattermost-redux/constants'; +import {Posts} from '@mm-redux/constants'; const basePostBodyProps = { postProps: { diff --git a/app/components/post_body_additional_content/index.js b/app/components/post_body_additional_content/index.js index b2ab09265..8ca3a5eb0 100644 --- a/app/components/post_body_additional_content/index.js +++ b/app/components/post_body_additional_content/index.js @@ -4,11 +4,11 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getRedirectLocation} from 'mattermost-redux/actions/general'; -import {Preferences} from 'mattermost-redux/constants'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getOpenGraphMetadataForUrl, getExpandedLink} from 'mattermost-redux/selectors/entities/posts'; -import {getBool, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getRedirectLocation} from '@mm-redux/actions/general'; +import {Preferences} from '@mm-redux/constants'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getOpenGraphMetadataForUrl, getExpandedLink} from '@mm-redux/selectors/entities/posts'; +import {getBool, getTheme} from '@mm-redux/selectors/entities/preferences'; import {ViewTypes} from 'app/constants'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index 1108de8e7..3c291ae64 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -14,7 +14,7 @@ import { import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube'; import {intlShape} from 'react-intl'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import PostAttachmentImage from 'app/components/post_attachment_image'; diff --git a/app/components/post_body_additional_content/post_body_additional_content.test.js b/app/components/post_body_additional_content/post_body_additional_content.test.js index 64bc6d3b0..b296f790c 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.test.js +++ b/app/components/post_body_additional_content/post_body_additional_content.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/components/post_header/index.js b/app/components/post_header/index.js index b3fbf97d3..ce390303f 100644 --- a/app/components/post_header/index.js +++ b/app/components/post_header/index.js @@ -3,15 +3,15 @@ import {connect} from 'react-redux'; -import {Preferences} from 'mattermost-redux/constants'; -import {makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts'; -import {getBool, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {getUser, getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {Preferences} from '@mm-redux/constants'; +import {makeGetCommentCountForPost} from '@mm-redux/selectors/entities/posts'; +import {getBool, getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {getUser, getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {isPostPendingOrFailed, isSystemMessage} from '@mm-redux/utils/post_utils'; +import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; +import {getConfig} from '@mm-redux/selectors/entities/general'; import {fromAutoResponder} from 'app/utils/general'; import {isGuest} from 'app/utils/users'; diff --git a/app/components/post_header/post_header.test.js b/app/components/post_header/post_header.test.js index c6cb0b103..c67a83820 100644 --- a/app/components/post_header/post_header.test.js +++ b/app/components/post_header/post_header.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import PostHeader from './post_header'; diff --git a/app/components/post_header/post_pre_header.test.js b/app/components/post_header/post_pre_header.test.js index 32df362c3..1289e7f8c 100644 --- a/app/components/post_header/post_pre_header.test.js +++ b/app/components/post_header/post_pre_header.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import PostPreHeader from './post_pre_header'; diff --git a/app/components/post_list/date_header/date_header.test.js b/app/components/post_list/date_header/date_header.test.js index abf84cebe..52f06e544 100644 --- a/app/components/post_list/date_header/date_header.test.js +++ b/app/components/post_list/date_header/date_header.test.js @@ -6,7 +6,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import DateHeader from './date_header.js'; diff --git a/app/components/post_list/date_header/index.js b/app/components/post_list/date_header/index.js index e56515ebc..1263cca5f 100644 --- a/app/components/post_list/date_header/index.js +++ b/app/components/post_list/date_header/index.js @@ -3,10 +3,10 @@ import {connect} from 'react-redux'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; import DateHeader from './date_header'; diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index 92168b4ba..38b604ee3 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectFocusedPostId} from 'mattermost-redux/actions/posts'; -import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'mattermost-redux/utils/post_list'; +import {selectFocusedPostId} from '@mm-redux/actions/posts'; +import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from '@mm-redux/utils/post_list'; import {handleSelectChannelByName, loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel'; import {setDeepLinkURL} from 'app/actions/views/root'; diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index ab860af88..ebc2af868 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -6,8 +6,8 @@ import PropTypes from 'prop-types'; import {Alert, FlatList, InteractionManager, Platform, RefreshControl, StyleSheet} from 'react-native'; import {intlShape} from 'react-intl'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import * as PostListUtils from 'mattermost-redux/utils/post_list'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import * as PostListUtils from '@mm-redux/utils/post_list'; import CombinedUserActivityPost from 'app/components/combined_user_activity_post'; import Post from 'app/components/post'; diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js index fb6d43e18..ea303d261 100644 --- a/app/components/post_list/post_list.test.js +++ b/app/components/post_list/post_list.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; import PostList from './post_list'; diff --git a/app/components/post_profile_picture/index.js b/app/components/post_profile_picture/index.js index c74c36a60..c4cff6bb5 100644 --- a/app/components/post_profile_picture/index.js +++ b/app/components/post_profile_picture/index.js @@ -3,17 +3,17 @@ import {connect} from 'react-redux'; -import {isSystemMessage} from 'mattermost-redux/utils/post_utils'; -import {Client4} from 'mattermost-redux/client'; +import {isSystemMessage} from '@mm-redux/utils/post_utils'; +import {Client4} from '@mm-redux/client'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getConfig} from '@mm-redux/selectors/entities/general'; import {fromAutoResponder} from 'app/utils/general'; import PostProfilePicture from './post_profile_picture'; -import {getUser} from 'mattermost-redux/selectors/entities/users'; +import {getUser} from '@mm-redux/selectors/entities/users'; function mapStateToProps(state, ownProps) { const config = getConfig(state); diff --git a/app/components/post_textbox/components/camera_button.test.js b/app/components/post_textbox/components/camera_button.test.js index b6dafee04..f4ab69afc 100644 --- a/app/components/post_textbox/components/camera_button.test.js +++ b/app/components/post_textbox/components/camera_button.test.js @@ -6,7 +6,7 @@ import {Alert, Platform, StatusBar} from 'react-native'; import {shallow} from 'enzyme'; import Permissions from 'react-native-permissions'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import CameraButton from './camera_button'; diff --git a/app/components/post_textbox/components/file_upload_button.test.js b/app/components/post_textbox/components/file_upload_button.test.js index 501e32c67..85fff73d8 100644 --- a/app/components/post_textbox/components/file_upload_button.test.js +++ b/app/components/post_textbox/components/file_upload_button.test.js @@ -6,7 +6,7 @@ import {Alert, Platform} from 'react-native'; import {shallow} from 'enzyme'; import Permissions from 'react-native-permissions'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import FileUploadButton from './file_upload_button'; diff --git a/app/components/post_textbox/components/image_upload_button.test.js b/app/components/post_textbox/components/image_upload_button.test.js index be82b39dc..b903f3f7d 100644 --- a/app/components/post_textbox/components/image_upload_button.test.js +++ b/app/components/post_textbox/components/image_upload_button.test.js @@ -6,7 +6,7 @@ import {Alert, Platform, StatusBar} from 'react-native'; import {shallow} from 'enzyme'; import Permissions from 'react-native-permissions'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ImageUploadButton from './image_upload_button'; diff --git a/app/components/post_textbox/components/typing/index.js b/app/components/post_textbox/components/typing/index.js index 0bbcb2a54..9283df8d7 100644 --- a/app/components/post_textbox/components/typing/index.js +++ b/app/components/post_textbox/components/typing/index.js @@ -3,9 +3,9 @@ import {connect} from 'react-redux'; -import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing'; +import {getUsersTyping} from '@mm-redux/selectors/entities/typing'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import Typing from './typing'; diff --git a/app/components/post_textbox/index.js b/app/components/post_textbox/index.js index 817962e2d..3e58d9a95 100644 --- a/app/components/post_textbox/index.js +++ b/app/components/post_textbox/index.js @@ -4,16 +4,16 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; -import {General, Permissions} from 'mattermost-redux/constants'; -import {createPost} from 'mattermost-redux/actions/posts'; -import {setStatus} from 'mattermost-redux/actions/users'; -import {getCurrentChannel, isCurrentChannelReadOnly, getCurrentChannelStats} from 'mattermost-redux/selectors/entities/channels'; -import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; -import {canUploadFilesOnMobile, getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; -import {getChannelTimezones} from 'mattermost-redux/actions/channels'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {General, Permissions} from '@mm-redux/constants'; +import {createPost} from '@mm-redux/actions/posts'; +import {setStatus} from '@mm-redux/actions/users'; +import {getCurrentChannel, isCurrentChannelReadOnly, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels'; +import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles'; +import {canUploadFilesOnMobile, getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users'; +import {getChannelTimezones} from '@mm-redux/actions/channels'; import {executeCommand} from 'app/actions/views/command'; import {addReactionToLatestPost} from 'app/actions/views/emoji'; diff --git a/app/components/post_textbox/post_textbox.test.js b/app/components/post_textbox/post_textbox.test.js index c6aaa4dc6..04afa4d93 100644 --- a/app/components/post_textbox/post_textbox.test.js +++ b/app/components/post_textbox/post_textbox.test.js @@ -6,8 +6,8 @@ import {Alert, Image} from 'react-native'; import assert from 'assert'; import {shallowWithIntl} from 'test/intl-test-helper'; -import Preferences from 'mattermost-redux/constants/preferences'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import Preferences from '@mm-redux/constants/preferences'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import SendButton from 'app/components/send_button'; import PasteableTextInput from 'app/components/pasteable_text_input'; diff --git a/app/components/post_textbox/post_textbox_base.js b/app/components/post_textbox/post_textbox_base.js index ead792b51..eb23dd2e3 100644 --- a/app/components/post_textbox/post_textbox_base.js +++ b/app/components/post_textbox/post_textbox_base.js @@ -24,9 +24,9 @@ import HWKeyboardEvent from 'react-native-hw-keyboard-event'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import slashForwardBoxIcon from 'assets/images/icons/slash-forward-box.png'; -import {General, RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils'; +import {General, RequestStatus} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {getFormattedFileSize} from '@mm-redux/utils/file_utils'; import FileUploadButton from './components/file_upload_button'; import ImageUploadButton from './components/image_upload_button'; diff --git a/app/components/profile_picture/index.js b/app/components/profile_picture/index.js index 3a77ef609..c9223ad51 100644 --- a/app/components/profile_picture/index.js +++ b/app/components/profile_picture/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getStatusesByIdsBatchedDebounced} from 'mattermost-redux/actions/users'; -import {getCurrentUserId, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getStatusesByIdsBatchedDebounced} from '@mm-redux/actions/users'; +import {getCurrentUserId, getStatusForUserId, getUser} from '@mm-redux/selectors/entities/users'; import {setProfileImageUri} from 'app/actions/views/edit_profile'; import {getProfileImageUri} from 'app/selectors/views'; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index c5668b718..7d0dc3995 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {Image, Platform, View} from 'react-native'; import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import UserStatus from 'app/components/user_status'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/components/profile_picture_button.js b/app/components/profile_picture_button.js index 07a495dc1..0c7fa08c2 100644 --- a/app/components/profile_picture_button.js +++ b/app/components/profile_picture_button.js @@ -4,7 +4,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import {t} from 'app/utils/i18n'; diff --git a/app/components/profile_picture_button.test.js b/app/components/profile_picture_button.test.js index 874c27bdd..13b603572 100644 --- a/app/components/profile_picture_button.test.js +++ b/app/components/profile_picture_button.test.js @@ -3,11 +3,11 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ProfilePictureButton from './profile_picture_button.js'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; jest.mock('react-native-image-picker', () => ({ launchCamera: jest.fn(), diff --git a/app/components/progressive_image/index.js b/app/components/progressive_image/index.js index 1fd5d11f0..aec264a51 100644 --- a/app/components/progressive_image/index.js +++ b/app/components/progressive_image/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import ProgressiveImage from './progressive_image'; diff --git a/app/components/progressive_image/progressive_image.test.js b/app/components/progressive_image/progressive_image.test.js index 7f4e1977c..dd82b2911 100644 --- a/app/components/progressive_image/progressive_image.test.js +++ b/app/components/progressive_image/progressive_image.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ProgressiveImage from './progressive_image'; diff --git a/app/components/radio_button/radio_button.js b/app/components/radio_button/radio_button.js index 2fea84759..50897b8ad 100644 --- a/app/components/radio_button/radio_button.js +++ b/app/components/radio_button/radio_button.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import {Animated, Text, View} from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; const DISABLED_OPACITY = 0.26; diff --git a/app/components/reactions/index.js b/app/components/reactions/index.js index a959f7b44..7e2882f6e 100644 --- a/app/components/reactions/index.js +++ b/app/components/reactions/index.js @@ -4,14 +4,14 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getReactionsForPost, removeReaction} from 'mattermost-redux/actions/posts'; -import {makeGetReactionsForPost, getPost} from 'mattermost-redux/selectors/entities/posts'; -import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; -import {hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; -import Permissions from 'mattermost-redux/constants/permissions'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getChannel, isChannelReadOnlyById} from 'mattermost-redux/selectors/entities/channels'; +import {getReactionsForPost, removeReaction} from '@mm-redux/actions/posts'; +import {makeGetReactionsForPost, getPost} from '@mm-redux/selectors/entities/posts'; +import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles'; +import {hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import Permissions from '@mm-redux/constants/permissions'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getChannel, isChannelReadOnlyById} from '@mm-redux/selectors/entities/channels'; import {addReaction} from 'app/actions/views/emoji'; import {MAX_ALLOWED_REACTIONS} from 'app/constants/emoji'; diff --git a/app/components/reactions/reactions.test.js b/app/components/reactions/reactions.test.js index 2e58d7757..48a2a0da3 100644 --- a/app/components/reactions/reactions.test.js +++ b/app/components/reactions/reactions.test.js @@ -6,7 +6,7 @@ import {shallow} from 'enzyme'; import Reactions from './reactions'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; describe('Reactions', () => { const baseProps = { diff --git a/app/components/retry_bar_indicator/index.js b/app/components/retry_bar_indicator/index.js index e1d893f95..d144e76ed 100644 --- a/app/components/retry_bar_indicator/index.js +++ b/app/components/retry_bar_indicator/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from '@mm-redux/constants'; import {getConnection} from 'app/selectors/device'; import RetryBarIndicator from './retry_bar_indicator'; diff --git a/app/components/root/index.js b/app/components/root/index.js index 410e264bc..a2050816f 100644 --- a/app/components/root/index.js +++ b/app/components/root/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentUrl} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getCurrentLocale} from 'app/selectors/i18n'; import {removeProtocol} from 'app/utils/url'; diff --git a/app/components/root/root.js b/app/components/root/root.js index 39672eb78..da1fd0480 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -6,8 +6,8 @@ import PropTypes from 'prop-types'; import {IntlProvider} from 'react-intl'; import {Platform} from 'react-native'; -import {Client4} from 'mattermost-redux/client'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {Client4} from '@mm-redux/client'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {resetToTeams} from 'app/actions/navigation'; import {NavigationTypes} from 'app/constants'; diff --git a/app/components/safe_area_view/index.js b/app/components/safe_area_view/index.js index f70a5245d..9ba90cb89 100644 --- a/app/components/safe_area_view/index.js +++ b/app/components/safe_area_view/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import SafeAreaView from './safe_area_view'; diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index deabefbf8..31eb1fbb9 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {Dimensions, Keyboard, NativeModules, View} from 'react-native'; import SafeArea from 'react-native-safe-area'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {DeviceTypes, ViewTypes} from 'app/constants'; import mattermostManaged from 'app/mattermost_managed'; diff --git a/app/components/safe_area_view/safe_area_view.ios.test.js b/app/components/safe_area_view/safe_area_view.ios.test.js index ba22b234f..d328e6fbe 100644 --- a/app/components/safe_area_view/safe_area_view.ios.test.js +++ b/app/components/safe_area_view/safe_area_view.ios.test.js @@ -4,7 +4,7 @@ import React from 'react'; import SafeArea from 'react-native-safe-area'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {DeviceTypes, ViewTypes} from 'app/constants'; import mattermostManaged from 'app/mattermost_managed'; diff --git a/app/components/send_button.test.js b/app/components/send_button.test.js index 9f670ce2d..d945b693e 100644 --- a/app/components/send_button.test.js +++ b/app/components/send_button.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import SendButton from 'app/components/send_button'; import {changeOpacity} from 'app/utils/theme'; diff --git a/app/components/show_more_button/show_more_button.test.js b/app/components/show_more_button/show_more_button.test.js index 145b419f6..07a690ac9 100644 --- a/app/components/show_more_button/show_more_button.test.js +++ b/app/components/show_more_button/show_more_button.test.js @@ -5,7 +5,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import LinearGradient from 'react-native-linear-gradient'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import ShowMoreButton from './index'; diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js index 476982370..3ca4df9ba 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.js @@ -10,7 +10,7 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import Badge from 'app/components/badge'; import ChannelIcon from 'app/components/channel_icon'; diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js index 9722f4cd6..63664a50c 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js @@ -5,7 +5,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import {TouchableHighlight} from 'react-native'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ChannelItem from './channel_item.js'; diff --git a/app/components/sidebars/main/channels_list/channel_item/index.js b/app/components/sidebars/main/channels_list/channel_item/index.js index 60c4141c2..396ce28ae 100644 --- a/app/components/sidebars/main/channels_list/channel_item/index.js +++ b/app/components/sidebars/main/channels_list/channel_item/index.js @@ -3,17 +3,17 @@ import {connect} from 'react-redux'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import { getCurrentChannelId, makeGetChannel, getMyChannelMember, shouldHideDefaultChannel, -} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; -import {getUserIdFromChannelName, isChannelMuted} from 'mattermost-redux/utils/channel_utils'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +} from '@mm-redux/selectors/entities/channels'; +import {getTheme, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; +import {getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import {isLandscape} from 'app/selectors/device'; import {getDraftForChannel} from 'app/selectors/views'; import {isGuest as isGuestUser} from 'app/utils/users'; diff --git a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js index 094226964..febb3112e 100644 --- a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js +++ b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js @@ -13,9 +13,9 @@ import { } from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; -import {General} from 'mattermost-redux/constants'; -import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {General} from '@mm-redux/constants'; +import {sortChannelsByDisplayName} from '@mm-redux/utils/channel_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import {t} from 'app/utils/i18n'; import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item'; import {ListTypes} from 'app/constants'; diff --git a/app/components/sidebars/main/channels_list/filtered_list/index.js b/app/components/sidebars/main/channels_list/filtered_list/index.js index a86581f0b..938ec8f0e 100644 --- a/app/components/sidebars/main/channels_list/filtered_list/index.js +++ b/app/components/sidebars/main/channels_list/filtered_list/index.js @@ -5,21 +5,21 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {createSelector} from 'reselect'; -import {searchChannels} from 'mattermost-redux/actions/channels'; -import {getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users'; -import {makeGroupMessageVisibleIfNecessary} from 'mattermost-redux/actions/preferences'; -import {General} from 'mattermost-redux/constants'; +import {searchChannels} from '@mm-redux/actions/channels'; +import {getProfilesInTeam, searchProfiles} from '@mm-redux/actions/users'; +import {makeGroupMessageVisibleIfNecessary} from '@mm-redux/actions/preferences'; +import {General} from '@mm-redux/constants'; import { getChannelsWithUnreadSection, getCurrentChannel, getGroupChannels, getArchivedChannels, getOtherChannels, -} from 'mattermost-redux/selectors/entities/channels'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from 'mattermost-redux/selectors/entities/users'; -import {getDirectShowPreferences, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +} from '@mm-redux/selectors/entities/channels'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getCurrentTeam} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from '@mm-redux/selectors/entities/users'; +import {getDirectShowPreferences, getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import Config from 'assets/config'; diff --git a/app/components/sidebars/main/channels_list/index.js b/app/components/sidebars/main/channels_list/index.js index 5613c86ba..0c1de974c 100644 --- a/app/components/sidebars/main/channels_list/index.js +++ b/app/components/sidebars/main/channels_list/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import ChannelsList from './channels_list'; diff --git a/app/components/sidebars/main/channels_list/list/index.js b/app/components/sidebars/main/channels_list/list/index.js index fc7ab6c66..97286566a 100644 --- a/app/components/sidebars/main/channels_list/list/index.js +++ b/app/components/sidebars/main/channels_list/list/index.js @@ -3,21 +3,21 @@ import {connect} from 'react-redux'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import { getSortedFavoriteChannelIds, getSortedUnreadChannelIds, getOrderedChannelIds, -} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getTheme, getFavoritesPreferences, getSidebarPreferences} from 'mattermost-redux/selectors/entities/preferences'; -import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; -import {memoizeResult} from 'mattermost-redux/utils/helpers'; -import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; -import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; -import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles'; -import Permissions from 'mattermost-redux/constants/permissions'; +} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId, getCurrentUserRoles} from '@mm-redux/selectors/entities/users'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getTheme, getFavoritesPreferences, getSidebarPreferences} from '@mm-redux/selectors/entities/preferences'; +import {showCreateOption} from '@mm-redux/utils/channel_utils'; +import {memoizeResult} from '@mm-redux/utils/helpers'; +import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils'; +import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import {haveITeamPermission} from '@mm-redux/selectors/entities/roles'; +import Permissions from '@mm-redux/constants/permissions'; import {isLandscape} from 'app/selectors/device'; import {DeviceTypes, ViewTypes} from 'app/constants'; diff --git a/app/components/sidebars/main/channels_list/list/list.js b/app/components/sidebars/main/channels_list/list/list.js index 84968377f..3c30be4ed 100644 --- a/app/components/sidebars/main/channels_list/list/list.js +++ b/app/components/sidebars/main/channels_list/list/list.js @@ -17,8 +17,8 @@ import {intlShape} from 'react-intl'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import FontAwesomePro from 'react-native-vector-icons/Ionicons'; -import {General} from 'mattermost-redux/constants'; -import {debounce} from 'mattermost-redux/actions/helpers'; +import {General} from '@mm-redux/constants'; +import {debounce} from '@mm-redux/actions/helpers'; import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item'; import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing'; diff --git a/app/components/sidebars/main/channels_list/switch_teams_button/index.js b/app/components/sidebars/main/channels_list/switch_teams_button/index.js index 6ebb64cea..8b688d548 100644 --- a/app/components/sidebars/main/channels_list/switch_teams_button/index.js +++ b/app/components/sidebars/main/channels_list/switch_teams_button/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId, getMyTeamsCount, getChannelDrawerBadgeCount} from 'mattermost-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId, getMyTeamsCount, getChannelDrawerBadgeCount} from '@mm-redux/selectors/entities/teams'; import SwitchTeamsButton from './switch_teams_button'; diff --git a/app/components/sidebars/main/drawer_swiper/index.js b/app/components/sidebars/main/drawer_swiper/index.js index e2f2fed68..d7a0b47fe 100644 --- a/app/components/sidebars/main/drawer_swiper/index.js +++ b/app/components/sidebars/main/drawer_swiper/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import DraweSwiper from './drawer_swiper'; diff --git a/app/components/sidebars/main/index.js b/app/components/sidebars/main/index.js index 5c252fd45..98d5cbc1b 100644 --- a/app/components/sidebars/main/index.js +++ b/app/components/sidebars/main/index.js @@ -4,11 +4,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {joinChannel} from 'mattermost-redux/actions/channels'; -import {getTeams} from 'mattermost-redux/actions/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {joinChannel} from '@mm-redux/actions/channels'; +import {getTeams} from '@mm-redux/actions/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId, getMyTeamsCount} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {setChannelDisplayName, handleSelectChannel} from 'app/actions/views/channel'; import {makeDirectChannel} from 'app/actions/views/more_dms'; diff --git a/app/components/sidebars/main/main_sidebar.android.js b/app/components/sidebars/main/main_sidebar.android.js index 1652eb7ce..37b3eb154 100644 --- a/app/components/sidebars/main/main_sidebar.android.js +++ b/app/components/sidebars/main/main_sidebar.android.js @@ -5,7 +5,7 @@ import React from 'react'; import {Dimensions, Keyboard} from 'react-native'; import {IntlProvider} from 'react-intl'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {closeMainSideMenu, enableMainSideMenu} from 'app/actions/navigation'; import {NavigationTypes} from 'app/constants'; diff --git a/app/components/sidebars/main/main_sidebar.ios.js b/app/components/sidebars/main/main_sidebar.ios.js index 96ede9833..362fa06a6 100644 --- a/app/components/sidebars/main/main_sidebar.ios.js +++ b/app/components/sidebars/main/main_sidebar.ios.js @@ -6,7 +6,7 @@ import {Dimensions, Keyboard} from 'react-native'; import {intlShape} from 'react-intl'; import AsyncStorage from '@react-native-community/async-storage'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import {DeviceTypes, NavigationTypes} from 'app/constants'; diff --git a/app/components/sidebars/main/main_sidebar.test.js b/app/components/sidebars/main/main_sidebar.test.js index 31759ac14..d8f892008 100644 --- a/app/components/sidebars/main/main_sidebar.test.js +++ b/app/components/sidebars/main/main_sidebar.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {DeviceTypes} from 'app/constants'; diff --git a/app/components/sidebars/main/main_sidebar_base.js b/app/components/sidebars/main/main_sidebar_base.js index 389b16ce6..5d42e138f 100644 --- a/app/components/sidebars/main/main_sidebar_base.js +++ b/app/components/sidebars/main/main_sidebar_base.js @@ -10,8 +10,8 @@ import { View, } from 'react-native'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import SafeAreaView from 'app/components/safe_area_view'; import {NavigationTypes, WebsocketEvents} from 'app/constants'; diff --git a/app/components/sidebars/main/teams_list/index.js b/app/components/sidebars/main/teams_list/index.js index 8d595920b..c28f647e1 100644 --- a/app/components/sidebars/main/teams_list/index.js +++ b/app/components/sidebars/main/teams_list/index.js @@ -4,8 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from '@mm-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {handleTeamChange} from 'app/actions/views/select_team'; import {getCurrentLocale} from 'app/selectors/i18n'; diff --git a/app/components/sidebars/main/teams_list/teams_list_item/index.js b/app/components/sidebars/main/teams_list/teams_list_item/index.js index 611d4a533..1dd38e98c 100644 --- a/app/components/sidebars/main/teams_list/teams_list_item/index.js +++ b/app/components/sidebars/main/teams_list/teams_list_item/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from '@mm-redux/selectors/entities/teams'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/components/sidebars/settings/index.js b/app/components/sidebars/settings/index.js index f404ca4cd..9ce1d0b1a 100644 --- a/app/components/sidebars/settings/index.js +++ b/app/components/sidebars/settings/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {setStatus} from 'mattermost-redux/actions/users'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; +import {setStatus} from '@mm-redux/actions/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUser, getStatusForUserId} from '@mm-redux/selectors/entities/users'; import {logout} from 'app/actions/views/user'; diff --git a/app/components/sidebars/settings/settings_sidebar.ios.js b/app/components/sidebars/settings/settings_sidebar.ios.js index 7455f7c0d..a8892e9e9 100644 --- a/app/components/sidebars/settings/settings_sidebar.ios.js +++ b/app/components/sidebars/settings/settings_sidebar.ios.js @@ -5,7 +5,7 @@ import React from 'react'; import {intlShape} from 'react-intl'; import {Dimensions, Keyboard, View} from 'react-native'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import SafeAreaView from 'app/components/safe_area_view'; import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; diff --git a/app/components/sidebars/settings/settings_sidebar_base.js b/app/components/sidebars/settings/settings_sidebar_base.js index c5723d6d9..34da5d1cd 100644 --- a/app/components/sidebars/settings/settings_sidebar_base.js +++ b/app/components/sidebars/settings/settings_sidebar_base.js @@ -6,8 +6,8 @@ import PropTypes from 'prop-types'; import {ScrollView, View} from 'react-native'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {showModal, showModalOverCurrentContext, dismissModal} from 'app/actions/navigation'; import UserStatus from 'app/components/user_status'; diff --git a/app/components/sidebars/settings/status_label/index.js b/app/components/sidebars/settings/status_label/index.js index 6c25bffd9..316ba6850 100644 --- a/app/components/sidebars/settings/status_label/index.js +++ b/app/components/sidebars/settings/status_label/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getStatusForUserId} from '@mm-redux/selectors/entities/users'; import StatusLabel from './status_label'; diff --git a/app/components/sidebars/settings/status_label/status_label.js b/app/components/sidebars/settings/status_label/status_label.js index 8aa4ea418..ad4522c1c 100644 --- a/app/components/sidebars/settings/status_label/status_label.js +++ b/app/components/sidebars/settings/status_label/status_label.js @@ -4,7 +4,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import FormattedText from 'app/components/formatted_text'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/components/sidebars/settings/user_info/index.js b/app/components/sidebars/settings/user_info/index.js index 47e30c0f7..920222f85 100644 --- a/app/components/sidebars/settings/user_info/index.js +++ b/app/components/sidebars/settings/user_info/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import UserInfo from './user_info'; diff --git a/app/components/status_bar/index.js b/app/components/status_bar/index.js index 5f4245faa..04f3f0628 100644 --- a/app/components/status_bar/index.js +++ b/app/components/status_bar/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import StatusBar from './status_bar'; diff --git a/app/components/team_icon/index.js b/app/components/team_icon/index.js index 663267d7e..dc980c565 100644 --- a/app/components/team_icon/index.js +++ b/app/components/team_icon/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTeam} from 'mattermost-redux/selectors/entities/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTeam} from '@mm-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import TeamIcon from './team_icon'; diff --git a/app/components/team_icon/team_icon.js b/app/components/team_icon/team_icon.js index 090fd4dd3..f4e6122cc 100644 --- a/app/components/team_icon/team_icon.js +++ b/app/components/team_icon/team_icon.js @@ -10,7 +10,7 @@ import { View, } from 'react-native'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/components/user_status/index.js b/app/components/user_status/index.js index c99c8553d..dcfaba448 100644 --- a/app/components/user_status/index.js +++ b/app/components/user_status/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getStatusForUserId} from '@mm-redux/selectors/entities/users'; import UserStatus from './user_status'; diff --git a/app/components/user_status/user_status.js b/app/components/user_status/user_status.js index 30caab880..500ab6129 100644 --- a/app/components/user_status/user_status.js +++ b/app/components/user_status/user_status.js @@ -4,7 +4,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import Icon from 'app/components/vector_icon'; diff --git a/app/components/user_status/user_status.test.js b/app/components/user_status/user_status.test.js index cf915d033..7bb8fdcba 100644 --- a/app/components/user_status/user_status.test.js +++ b/app/components/user_status/user_status.test.js @@ -4,8 +4,8 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {General} from 'mattermost-redux/constants'; -import Preferences from 'mattermost-redux/constants/preferences'; +import {General} from '@mm-redux/constants'; +import Preferences from '@mm-redux/constants/preferences'; import UserStatus from './user_status'; diff --git a/app/components/widgets/settings/bool_setting.test.js b/app/components/widgets/settings/bool_setting.test.js index 386dd0651..54827d1df 100644 --- a/app/components/widgets/settings/bool_setting.test.js +++ b/app/components/widgets/settings/bool_setting.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import BoolSetting from './bool_setting.js'; diff --git a/app/components/widgets/settings/radio_setting.test.js b/app/components/widgets/settings/radio_setting.test.js index 0fab23c94..1d3614c5f 100644 --- a/app/components/widgets/settings/radio_setting.test.js +++ b/app/components/widgets/settings/radio_setting.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {TouchableOpacity} from 'react-native'; import RadioSetting from './radio_setting.js'; import CheckMark from 'app/components/checkmark'; diff --git a/app/components/widgets/settings/text_setting.test.js b/app/components/widgets/settings/text_setting.test.js index 740f8c691..dac702911 100644 --- a/app/components/widgets/settings/text_setting.test.js +++ b/app/components/widgets/settings/text_setting.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import TextSetting from './text_setting.js'; diff --git a/app/constants/device.js b/app/constants/device.js index b71388c87..a46bbc035 100644 --- a/app/constants/device.js +++ b/app/constants/device.js @@ -3,7 +3,7 @@ import DeviceInfo from 'react-native-device-info'; import RNFetchBlobFS from 'rn-fetch-blob/fs'; -import keyMirror from 'mattermost-redux/utils/key_mirror'; +import keyMirror from '@mm-redux/utils/key_mirror'; const deviceTypes = keyMirror({ CONNECTION_CHANGED: null, diff --git a/app/constants/navigation.js b/app/constants/navigation.js index df458f39f..04758278c 100644 --- a/app/constants/navigation.js +++ b/app/constants/navigation.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import keyMirror from 'mattermost-redux/utils/key_mirror'; +import keyMirror from '@mm-redux/utils/key_mirror'; const NavigationTypes = keyMirror({ NAVIGATION_RESET: null, diff --git a/app/constants/view.js b/app/constants/view.js index 491415717..f2e5c4a1f 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import keyMirror from 'mattermost-redux/utils/key_mirror'; +import keyMirror from '@mm-redux/utils/key_mirror'; import DeviceInfo from 'react-native-device-info'; // The iPhone 11 and iPhone 11 Pro Max have a navbar height of 44 and iPhone 11 Pro has 32 diff --git a/app/init/credentials.js b/app/init/credentials.js index ba4dc7f0e..67569f6d2 100644 --- a/app/init/credentials.js +++ b/app/init/credentials.js @@ -4,7 +4,7 @@ import AsyncStorage from '@react-native-community/async-storage'; import * as KeyChain from 'react-native-keychain'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import mattermostManaged from 'app/mattermost_managed'; import EphemeralStore from 'app/store/ephemeral_store'; diff --git a/app/init/fetch.js b/app/init/fetch.js index 38e08fb7a..0ea1180e5 100644 --- a/app/init/fetch.js +++ b/app/init/fetch.js @@ -6,10 +6,10 @@ import DeviceInfo from 'react-native-device-info'; import RNFetchBlob from 'rn-fetch-blob'; import urlParse from 'url-parse'; -import {Client4} from 'mattermost-redux/client'; -import {ClientError, HEADER_X_VERSION_ID} from 'mattermost-redux/client/client4'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {General} from 'mattermost-redux/constants'; +import {Client4} from '@mm-redux/client'; +import {ClientError, HEADER_X_VERSION_ID} from '@mm-redux/client/client4'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {General} from '@mm-redux/constants'; import mattermostBucket from 'app/mattermost_bucket'; import mattermostManaged from 'app/mattermost_managed'; diff --git a/app/init/fetch.test.js b/app/init/fetch.test.js index 87d5e179a..1aac7e2d8 100644 --- a/app/init/fetch.test.js +++ b/app/init/fetch.test.js @@ -1,10 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; -import {HEADER_X_VERSION_ID} from 'mattermost-redux/client/client4'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {General} from 'mattermost-redux/constants'; +import {Client4} from '@mm-redux/client'; +import {HEADER_X_VERSION_ID} from '@mm-redux/client/client4'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {General} from '@mm-redux/constants'; import { HEADER_X_CLUSTER_ID, diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index de0f1786d..347aec9a3 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -9,16 +9,16 @@ import RNFetchBlob from 'rn-fetch-blob'; import {batchActions} from 'redux-batched-actions'; import semver from 'semver/preload'; -import {setAppState, setServerVersion} from 'mattermost-redux/actions/general'; -import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; +import {setAppState, setServerVersion} from '@mm-redux/actions/general'; +import {autoUpdateTimezone} from '@mm-redux/actions/timezone'; import {close as closeWebSocket} from '@actions/websocket'; -import {GeneralTypes} from 'mattermost-redux/action_types'; -import {Client4} from 'mattermost-redux/client'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; +import {GeneralTypes} from '@mm-redux/action_types'; +import {Client4} from '@mm-redux/client'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device'; import {selectDefaultChannel} from 'app/actions/views/channel'; diff --git a/app/init/global_event_handler.test.js b/app/init/global_event_handler.test.js index beb29e1a1..53c9b6ea7 100644 --- a/app/init/global_event_handler.test.js +++ b/app/init/global_event_handler.test.js @@ -39,7 +39,7 @@ jest.mock('react-native-status-bar-size', () => ({ addEventListener: jest.fn(), })); -jest.mock('mattermost-redux/actions/general', () => ({ +jest.mock('@mm-redux/actions/general', () => ({ setAppState: jest.fn(), setServerVersion: jest.fn().mockReturnValue('setServerVersion'), })); diff --git a/app/mattermost.js b/app/mattermost.js index 1cc5ee5d5..0dde4802a 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -5,7 +5,7 @@ import {Linking} from 'react-native'; import {Navigation} from 'react-native-navigation'; import {Provider} from 'react-redux'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {loadMe} from 'app/actions/views/user'; diff --git a/app/mm-redux/action_types/bots.ts b/app/mm-redux/action_types/bots.ts new file mode 100644 index 000000000..f7ac9929b --- /dev/null +++ b/app/mm-redux/action_types/bots.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + RECEIVED_BOT_ACCOUNTS: null, + RECEIVED_BOT_ACCOUNT: null, +}); diff --git a/app/mm-redux/action_types/channel_categories.ts b/app/mm-redux/action_types/channel_categories.ts new file mode 100644 index 000000000..b50592482 --- /dev/null +++ b/app/mm-redux/action_types/channel_categories.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + RECEIVED_CATEGORY: null, + RECEIVED_CATEGORIES: null, + RECEIVED_CATEGORY_ORDER: null, + + CATEGORY_COLLAPSED: null, + CATEGORY_EXPANDED: null, + + CATEGORY_DELETED: null, +}); diff --git a/app/mm-redux/action_types/channels.ts b/app/mm-redux/action_types/channels.ts new file mode 100644 index 000000000..08db9cacf --- /dev/null +++ b/app/mm-redux/action_types/channels.ts @@ -0,0 +1,90 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + CHANNEL_REQUEST: null, + CHANNEL_SUCCESS: null, + CHANNEL_FAILURE: null, + + CHANNELS_REQUEST: null, + CHANNELS_SUCCESS: null, + CHANNELS_FAILURE: null, + + CREATE_CHANNEL_REQUEST: null, + CREATE_CHANNEL_SUCCESS: null, + CREATE_CHANNEL_FAILURE: null, + + UPDATE_CHANNEL_REQUEST: null, + UPDATE_CHANNEL_SUCCESS: null, + UPDATE_CHANNEL_FAILURE: null, + + DELETE_CHANNEL_SUCCESS: null, + UNARCHIVED_CHANNEL_SUCCESS: null, + + GET_CHANNELS_REQUEST: null, + GET_CHANNELS_SUCCESS: null, + GET_CHANNELS_FAILURE: null, + + GET_ALL_CHANNELS_REQUEST: null, + GET_ALL_CHANNELS_SUCCESS: null, + GET_ALL_CHANNELS_FAILURE: null, + + GET_CHANNELS_TIMEZONE_REQUEST: null, + GET_CHANNELS_TIMEZONE_SUCCESS: null, + GET_CHANNELS_TIMEZONE_FAILURE: null, + + CHANNEL_STATS_REQUEST: null, + CHANNEL_STATS_SUCCESS: null, + CHANNEL_STATS_FAILURE: null, + + ADD_CHANNEL_MEMBER_REQUEST: null, + ADD_CHANNEL_MEMBER_SUCCESS: null, + + REMOVE_CHANNEL_MEMBER_SUCCESS: null, + + SELECT_CHANNEL: null, + LEAVE_CHANNEL: null, + REMOVE_MEMBER_FROM_CHANNEL: null, + RECEIVED_CHANNEL: null, + RECEIVED_CHANNELS: null, + RECEIVED_ALL_CHANNELS: null, + RECEIVED_CHANNELS_LIST: null, + RECEIVED_MY_CHANNEL_MEMBERS: null, + RECEIVED_MY_CHANNEL_MEMBER: null, + RECEIVED_CHANNEL_MEMBERS: null, + RECEIVED_CHANNEL_MEMBER: null, + RECEIVED_CHANNEL_STATS: null, + RECEIVED_CHANNEL_PROPS: null, + RECEIVED_CHANNEL_DELETED: null, + RECEIVED_CHANNEL_UNARCHIVED: null, + RECEIVED_LAST_VIEWED_AT: null, + UPDATE_CHANNEL_HEADER: null, + UPDATE_CHANNEL_PURPOSE: null, + CHANNEL_MEMBER_ADDED: null, + CHANNEL_MEMBER_REMOVED: null, + + INCREMENT_TOTAL_MSG_COUNT: null, + INCREMENT_UNREAD_MSG_COUNT: null, + DECREMENT_UNREAD_MSG_COUNT: null, + INCREMENT_UNREAD_MENTION_COUNT: null, + DECREMENT_UNREAD_MENTION_COUNT: null, + + UPDATED_CHANNEL_SCHEME: null, + UPDATED_CHANNEL_MEMBER_SCHEME_ROLES: null, + + RECEIVED_CHANNEL_MEMBERS_MINUS_GROUP_MEMBERS: null, + + RECEIVED_CHANNEL_MODERATIONS: null, + + RECEIVED_TOTAL_CHANNEL_COUNT: null, + + POST_UNREAD_SUCCESS: null, + + ADD_MANUALLY_UNREAD: null, + REMOVE_MANUALLY_UNREAD: null, + RECEIVED_MY_CHANNELS_WITH_MEMBERS: null, + + INCREMENT_PINNED_POST_COUNT: null, + DECREMENT_PINNED_POST_COUNT: null, +}); diff --git a/app/mm-redux/action_types/emojis.ts b/app/mm-redux/action_types/emojis.ts new file mode 100644 index 000000000..4efa09a84 --- /dev/null +++ b/app/mm-redux/action_types/emojis.ts @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + CLEAR_CUSTOM_EMOJIS: null, + RECEIVED_CUSTOM_EMOJI: null, + RECEIVED_CUSTOM_EMOJIS: null, + DELETED_CUSTOM_EMOJI: null, + CUSTOM_EMOJI_DOES_NOT_EXIST: null, +}); diff --git a/app/mm-redux/action_types/errors.ts b/app/mm-redux/action_types/errors.ts new file mode 100644 index 000000000..f59d781ed --- /dev/null +++ b/app/mm-redux/action_types/errors.ts @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + DISMISS_ERROR: null, + LOG_ERROR: null, + CLEAR_ERRORS: null, + RESTORE_ERRORS: null, +}); diff --git a/app/mm-redux/action_types/files.ts b/app/mm-redux/action_types/files.ts new file mode 100644 index 000000000..2a100f6ba --- /dev/null +++ b/app/mm-redux/action_types/files.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; +export default keyMirror({ + + UPLOAD_FILES_REQUEST: null, + UPLOAD_FILES_SUCCESS: null, + UPLOAD_FILES_FAILURE: null, + UPLOAD_FILES_CANCEL: null, + + RECEIVED_FILES_FOR_POST: null, + RECEIVED_UPLOAD_FILES: null, + RECEIVED_FILE_PUBLIC_LINK: null, +}); diff --git a/app/mm-redux/action_types/general.ts b/app/mm-redux/action_types/general.ts new file mode 100644 index 000000000..d5f11b835 --- /dev/null +++ b/app/mm-redux/action_types/general.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + RECEIVED_APP_STATE: null, + RECEIVED_APP_CREDENTIALS: null, + REMOVED_APP_CREDENTIALS: null, + RECEIVED_APP_DEVICE_TOKEN: null, + + PING_RESET: null, + + RECEIVED_SERVER_VERSION: null, + + CLIENT_CONFIG_RECEIVED: null, + CLIENT_CONFIG_RESET: null, + + CLIENT_LICENSE_RECEIVED: null, + CLIENT_LICENSE_RESET: null, + + RECEIVED_DATA_RETENTION_POLICY: null, + + LOG_CLIENT_ERROR_REQUEST: null, + LOG_CLIENT_ERROR_SUCCESS: null, + LOG_CLIENT_ERROR_FAILURE: null, + + SUPPORTED_TIMEZONES_REQUEST: null, + SUPPORTED_TIMEZONES_SUCCESS: null, + SUPPORTED_TIMEZONES_FAILURE: null, + SUPPORTED_TIMEZONES_RECEIVED: null, + + WEBSOCKET_REQUEST: null, + WEBSOCKET_SUCCESS: null, + WEBSOCKET_FAILURE: null, + WEBSOCKET_CLOSED: null, + + REDIRECT_LOCATION_SUCCESS: null, + REDIRECT_LOCATION_FAILURE: null, + SET_CONFIG_AND_LICENSE: null, +}); diff --git a/app/mm-redux/action_types/gifs.ts b/app/mm-redux/action_types/gifs.ts new file mode 100644 index 000000000..39761ae3f --- /dev/null +++ b/app/mm-redux/action_types/gifs.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + + // General. + + SAVE_APP_PROPS: null, + + // Search. + + SEARCH_REQUEST: null, + SEARCH_FAILURE: null, + SEARCH_SUCCESS: null, + + SEARCH_BY_ID_REQUEST: null, + SEARCH_BY_ID_FAILURE: null, + SEARCH_BY_ID_SUCCESS: null, + + SELECT_SEARCH_TEXT: null, + INVALIDATE_SEARCH_TEXT: null, + + REQUEST_SEARCH: null, + RECEIVE_SEARCH: null, + RECEIVE_SEARCH_END: null, + + RECEIVE_CATEGORY_SEARCH: null, + + CLEAR_SEARCH_RESULTS: null, + + SAVE_SEARCH_SCROLL_POSITION: null, + SAVE_SEARCH_PRIOR_LOCATION: null, + + UPDATE_SEARCH_TEXT: null, + SAVE_SEARCH_BAR_TEXT: null, + + // Categories. + + REQUEST_CATEGORIES_LIST: null, + CATEGORIES_LIST_RECEIVED: null, + CATEGORIES_LIST_FAILURE: null, + + // Cache. + + CACHE_GIFS: null, + CACHE_REQUEST: null, +}); diff --git a/app/mm-redux/action_types/groups.ts b/app/mm-redux/action_types/groups.ts new file mode 100644 index 000000000..695b18f0c --- /dev/null +++ b/app/mm-redux/action_types/groups.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + LINKED_GROUP_TEAM: null, + LINKED_GROUP_CHANNEL: null, + + UNLINKED_GROUP_TEAM: null, + UNLINKED_GROUP_CHANNEL: null, + + RECEIVED_GROUP_TEAMS: null, + RECEIVED_GROUP_CHANNELS: null, + + RECEIVED_GROUP_MEMBERS: null, + + RECEIVED_GROUP: null, + + RECEIVED_GROUPS: null, + + RECEIVED_GROUPS_ASSOCIATED_TO_TEAM: null, + + RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL: null, + + RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM: null, + + RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNEL: null, + + RECEIVED_GROUPS_NOT_ASSOCIATED_TO_TEAM: null, + + RECEIVED_GROUPS_NOT_ASSOCIATED_TO_CHANNEL: null, + + PATCHED_GROUP_TEAM: null, + PATCHED_GROUP_CHANNEL: null, +}); diff --git a/app/mm-redux/action_types/index.ts b/app/mm-redux/action_types/index.ts new file mode 100644 index 000000000..305bf3a1e --- /dev/null +++ b/app/mm-redux/action_types/index.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import ChannelTypes from './channels'; +import ErrorTypes from './errors'; +import GeneralTypes from './general'; +import UserTypes from './users'; +import TeamTypes from './teams'; +import PostTypes from './posts'; +import FileTypes from './files'; +import PreferenceTypes from './preferences'; +import IntegrationTypes from './integrations'; +import EmojiTypes from './emojis'; +import GifTypes from './gifs'; +import JobTypes from './jobs'; +import SearchTypes from './search'; +import RoleTypes from './roles'; +import SchemeTypes from './schemes'; +import GroupTypes from './groups'; +import BotTypes from './bots'; +import PluginTypes from './plugins'; +import ChannelCategoryTypes from './channel_categories'; + +export { + ErrorTypes, + GeneralTypes, + UserTypes, + TeamTypes, + ChannelTypes, + PostTypes, + FileTypes, + PreferenceTypes, + IntegrationTypes, + EmojiTypes, + GifTypes, + JobTypes, + SearchTypes, + RoleTypes, + SchemeTypes, + GroupTypes, + BotTypes, + PluginTypes, + ChannelCategoryTypes, +}; diff --git a/app/mm-redux/action_types/integrations.ts b/app/mm-redux/action_types/integrations.ts new file mode 100644 index 000000000..ecb021f59 --- /dev/null +++ b/app/mm-redux/action_types/integrations.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + + RECEIVED_INCOMING_HOOK: null, + RECEIVED_INCOMING_HOOKS: null, + DELETED_INCOMING_HOOK: null, + RECEIVED_OUTGOING_HOOK: null, + RECEIVED_OUTGOING_HOOKS: null, + DELETED_OUTGOING_HOOK: null, + RECEIVED_CUSTOM_TEAM_COMMANDS: null, + RECEIVED_COMMAND: null, + RECEIVED_COMMANDS: null, + RECEIVED_COMMAND_TOKEN: null, + DELETED_COMMAND: null, + RECEIVED_OAUTH_APP: null, + RECEIVED_OAUTH_APPS: null, + DELETED_OAUTH_APP: null, + + RECEIVED_DIALOG_TRIGGER_ID: null, + RECEIVED_DIALOG: null, +}); diff --git a/app/mm-redux/action_types/jobs.ts b/app/mm-redux/action_types/jobs.ts new file mode 100644 index 000000000..f3888e954 --- /dev/null +++ b/app/mm-redux/action_types/jobs.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + CREATE_JOB_REQUEST: null, + CREATE_JOB_SUCCESS: null, + CREATE_JOB_FAILURE: null, + + CANCEL_JOB_REQUEST: null, + CANCEL_JOB_SUCCESS: null, + CANCEL_JOB_FAILURE: null, + + GET_JOB_REQUEST: null, + GET_JOB_SUCCESS: null, + GET_JOB_FAILURE: null, + + GET_JOBS_REQUEST: null, + GET_JOBS_SUCCESS: null, + GET_JOBS_FAILURE: null, + + RECEIVED_JOB: null, + RECEIVED_JOBS: null, + RECEIVED_JOBS_BY_TYPE: null, +}); diff --git a/app/mm-redux/action_types/plugins.ts b/app/mm-redux/action_types/plugins.ts new file mode 100644 index 000000000..c4c84296f --- /dev/null +++ b/app/mm-redux/action_types/plugins.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + RECEIVED_MARKETPLACE_PLUGINS: null, + GET_MARKETPLACE_PLUGINS_FAILURE: null, +}); diff --git a/app/mm-redux/action_types/posts.ts b/app/mm-redux/action_types/posts.ts new file mode 100644 index 000000000..8f84b4127 --- /dev/null +++ b/app/mm-redux/action_types/posts.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + CREATE_POST_REQUEST: null, + CREATE_POST_SUCCESS: null, + CREATE_POST_FAILURE: null, + CREATE_POST_RESET_REQUEST: null, + + EDIT_POST_REQUEST: null, + EDIT_POST_SUCCESS: null, + EDIT_POST_FAILURE: null, + + GET_POST_THREAD_REQUEST: null, + GET_POST_THREAD_SUCCESS: null, + GET_POST_THREAD_FAILURE: null, + + GET_POSTS_SUCCESS: null, + GET_POSTS_FAILURE: null, + GET_POSTS_SINCE_SUCCESS: null, + + GET_POST_THREAD_WITH_RETRY_ATTEMPT: null, + GET_POSTS_WITH_RETRY_ATTEMPT: null, + GET_POSTS_SINCE_WITH_RETRY_ATTEMPT: null, + GET_POSTS_BEFORE_WITH_RETRY_ATTEMPT: null, + GET_POSTS_AFTER_WITH_RETRY_ATTEMPT: null, + + RECEIVED_POST: null, + RECEIVED_NEW_POST: null, + + RECEIVED_POSTS: null, + RECEIVED_POSTS_AFTER: null, + RECEIVED_POSTS_BEFORE: null, + RECEIVED_POSTS_IN_CHANNEL: null, + RECEIVED_POSTS_IN_THREAD: null, + RECEIVED_POSTS_SINCE: null, + + POST_DELETED: null, + POST_REMOVED: null, + + RECEIVED_FOCUSED_POST: null, + RECEIVED_POST_SELECTED: null, + RECEIVED_EDIT_POST: null, + RECEIVED_REACTION: null, + RECEIVED_REACTIONS: null, + REACTION_DELETED: null, + RECEIVED_OPEN_GRAPH_METADATA: null, + + ADD_MESSAGE_INTO_HISTORY: null, + RESET_HISTORY_INDEX: null, + MOVE_HISTORY_INDEX_BACK: null, + MOVE_HISTORY_INDEX_FORWARD: null, +}); diff --git a/app/mm-redux/action_types/preferences.ts b/app/mm-redux/action_types/preferences.ts new file mode 100644 index 000000000..1b0c0a117 --- /dev/null +++ b/app/mm-redux/action_types/preferences.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + RECEIVED_PREFERENCES: null, + RECEIVED_ALL_PREFERENCES: null, + DELETED_PREFERENCES: null, +}); diff --git a/app/mm-redux/action_types/roles.ts b/app/mm-redux/action_types/roles.ts new file mode 100644 index 000000000..ebd4dd580 --- /dev/null +++ b/app/mm-redux/action_types/roles.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + ROLES_BY_NAMES_REQUEST: null, + ROLES_BY_NAMES_SUCCESS: null, + ROLES_BY_NAMES_FAILURE: null, + + ROLE_BY_NAME_REQUEST: null, + ROLE_BY_NAME_SUCCESS: null, + ROLE_BY_NAME_FAILURE: null, + + ROLE_BY_ID_REQUEST: null, + ROLE_BY_ID_SUCCESS: null, + ROLE_BY_ID_FAILURE: null, + + EDIT_ROLE_REQUEST: null, + EDIT_ROLE_SUCCESS: null, + EDIT_ROLE_FAILURE: null, + + RECEIVED_ROLES: null, + RECEIVED_ROLE: null, + ROLE_DELETED: null, + + SET_PENDING_ROLES: null, +}); diff --git a/app/mm-redux/action_types/schemes.ts b/app/mm-redux/action_types/schemes.ts new file mode 100644 index 000000000..2f5a5d87f --- /dev/null +++ b/app/mm-redux/action_types/schemes.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + RECEIVED_SCHEME: null, + + RECEIVED_SCHEMES: null, + + CREATED_SCHEME: null, + + DELETED_SCHEME: null, + + PATCHED_SCHEME: null, + + RECEIVED_SCHEME_TEAMS: null, + + RECEIVED_SCHEME_CHANNELS: null, +}); diff --git a/app/mm-redux/action_types/search.ts b/app/mm-redux/action_types/search.ts new file mode 100644 index 000000000..df4b4293d --- /dev/null +++ b/app/mm-redux/action_types/search.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + SEARCH_POSTS_REQUEST: null, + SEARCH_POSTS_SUCCESS: null, + + SEARCH_FLAGGED_POSTS_REQUEST: null, + SEARCH_FLAGGED_POSTS_SUCCESS: null, + SEARCH_FLAGGED_POSTS_FAILURE: null, + + SEARCH_PINNED_POSTS_REQUEST: null, + SEARCH_PINNED_POSTS_SUCCESS: null, + SEARCH_PINNED_POSTS_FAILURE: null, + REMOVE_SEARCH_PINNED_POSTS: null, + RECEIVED_SEARCH_POSTS: null, + RECEIVED_SEARCH_FLAGGED_POSTS: null, + RECEIVED_SEARCH_PINNED_POSTS: null, + RECEIVED_SEARCH_TERM: null, + REMOVE_SEARCH_POSTS: null, + REMOVE_SEARCH_TERM: null, +}); diff --git a/app/mm-redux/action_types/teams.ts b/app/mm-redux/action_types/teams.ts new file mode 100644 index 000000000..994446cb3 --- /dev/null +++ b/app/mm-redux/action_types/teams.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + GET_TEAMS_REQUEST: null, + GET_TEAMS_SUCCESS: null, + GET_TEAMS_FAILURE: null, + + MY_TEAMS_REQUEST: null, + MY_TEAMS_SUCCESS: null, + MY_TEAMS_FAILURE: null, + + CREATE_TEAM_REQUEST: null, + CREATE_TEAM_SUCCESS: null, + CREATE_TEAM_FAILURE: null, + + GET_TEAM_MEMBERS_REQUEST: null, + GET_TEAM_MEMBERS_SUCCESS: null, + GET_TEAM_MEMBERS_FAILURE: null, + + JOIN_TEAM_REQUEST: null, + JOIN_TEAM_SUCCESS: null, + JOIN_TEAM_FAILURE: null, + + TEAM_INVITE_INFO_REQUEST: null, + TEAM_INVITE_INFO_SUCCESS: null, + TEAM_INVITE_INFO_FAILURE: null, + + ADD_TO_TEAM_FROM_INVITE_REQUEST: null, + ADD_TO_TEAM_FROM_INVITE_SUCCESS: null, + ADD_TO_TEAM_FROM_INVITE_FAILURE: null, + + CREATED_TEAM: null, + SELECT_TEAM: null, + UPDATED_TEAM: null, + PATCHED_TEAM: null, + REGENERATED_TEAM_INVITE_ID: null, + RECEIVED_TEAM: null, + RECEIVED_TEAMS: null, + RECEIVED_TEAM_DELETED: null, + RECEIVED_TEAMS_LIST: null, + RECEIVED_MY_TEAM_MEMBERS: null, + RECEIVED_MY_TEAM_MEMBER: null, + RECEIVED_TEAM_MEMBERS: null, + RECEIVED_MEMBERS_IN_TEAM: null, + RECEIVED_MEMBER_IN_TEAM: null, + REMOVE_MEMBER_FROM_TEAM: null, + RECEIVED_TEAM_STATS: null, + RECEIVED_MY_TEAM_UNREADS: null, + LEAVE_TEAM: null, + UPDATED_TEAM_SCHEME: null, + UPDATED_TEAM_MEMBER_SCHEME_ROLES: null, + + RECEIVED_TEAM_MEMBERS_MINUS_GROUP_MEMBERS: null, + + RECEIVED_TOTAL_TEAM_COUNT: null, +}); diff --git a/app/mm-redux/action_types/users.ts b/app/mm-redux/action_types/users.ts new file mode 100644 index 000000000..080740a3b --- /dev/null +++ b/app/mm-redux/action_types/users.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + CREATE_USER_REQUEST: null, + CREATE_USER_SUCCESS: null, + CREATE_USER_FAILURE: null, + + LOGIN_REQUEST: null, + LOGIN_SUCCESS: null, + LOGIN_FAILURE: null, + + LOGOUT_REQUEST: null, + LOGOUT_SUCCESS: null, + LOGOUT_FAILURE: null, + + REVOKE_ALL_USER_SESSIONS_SUCCESS: null, + REVOKE_SESSIONS_FOR_ALL_USERS_SUCCESS: null, + + CHECK_MFA_REQUEST: null, + CHECK_MFA_SUCCESS: null, + CHECK_MFA_FAILURE: null, + + AUTOCOMPLETE_USERS_REQUEST: null, + AUTOCOMPLETE_USERS_SUCCESS: null, + AUTOCOMPLETE_USERS_FAILURE: null, + + UPDATE_ME_REQUEST: null, + UPDATE_ME_SUCCESS: null, + UPDATE_ME_FAILURE: null, + + RECEIVED_ME: null, + RECEIVED_TERMS_OF_SERVICE_STATUS: null, + RECEIVED_PROFILE: null, + RECEIVED_PROFILES: null, + RECEIVED_PROFILES_LIST: null, + RECEIVED_PROFILES_IN_TEAM: null, + RECEIVED_PROFILE_IN_TEAM: null, + RECEIVED_PROFILES_LIST_IN_TEAM: null, + RECEIVED_PROFILE_NOT_IN_TEAM: null, + RECEIVED_PROFILES_LIST_NOT_IN_TEAM: null, + RECEIVED_PROFILES_LIST_NOT_IN_TEAM_AND_REPLACE: null, + RECEIVED_PROFILE_WITHOUT_TEAM: null, + RECEIVED_PROFILES_LIST_WITHOUT_TEAM: null, + RECEIVED_PROFILES_IN_CHANNEL: null, + RECEIVED_PROFILES_LIST_IN_CHANNEL: null, + RECEIVED_PROFILE_IN_CHANNEL: null, + RECEIVED_PROFILES_NOT_IN_CHANNEL: null, + RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL: null, + RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL_AND_REPLACE: null, + RECEIVED_PROFILE_NOT_IN_CHANNEL: null, + RECEIVED_SESSIONS: null, + RECEIVED_REVOKED_SESSION: null, + RECEIVED_AUDITS: null, + RECEIVED_STATUS: null, + RECEIVED_STATUSES: null, + RECEIVED_AUTOCOMPLETE_IN_CHANNEL: null, + RESET_LOGOUT_STATE: null, + RECEIVED_MY_USER_ACCESS_TOKEN: null, + RECEIVED_MY_USER_ACCESS_TOKENS: null, + CLEAR_MY_USER_ACCESS_TOKENS: null, + REVOKED_USER_ACCESS_TOKEN: null, + DISABLED_USER_ACCESS_TOKEN: null, + ENABLED_USER_ACCESS_TOKEN: null, + RECEIVED_USER_STATS: null, + PROFILE_NO_LONGER_VISIBLE: null, + LOGIN: null, + RECEIVED_BATCHED_PROFILES_IN_CHANNEL: null, +}); diff --git a/app/mm-redux/actions/bots.test.js b/app/mm-redux/actions/bots.test.js new file mode 100644 index 000000000..d56120880 --- /dev/null +++ b/app/mm-redux/actions/bots.test.js @@ -0,0 +1,152 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as BotActions from '@mm-redux/actions/bots'; +import * as UserActions from '@mm-redux/actions/users'; +import {Client4} from '@mm-redux/client'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.Bots', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('loadBots', async () => { + const bots = [TestHelper.fakeBot(), TestHelper.fakeBot()]; + nock(Client4.getBaseRoute()). + get('/bots'). + query(true). + reply(201, bots); + + await store.dispatch(BotActions.loadBots()); + + const state = store.getState(); + const botsResult = state.entities.bots.accounts; + assert.equal(bots.length, Object.values(botsResult).length); + }); + + it('loadBot', async () => { + const bot = TestHelper.fakeBot(); + nock(Client4.getBaseRoute()). + get(`/bots/${bot.user_id}`). + query(true). + reply(201, bot); + + await store.dispatch(BotActions.loadBot(bot.user_id)); + + const state = store.getState(); + const botsResult = state.entities.bots.accounts[bot.user_id]; + assert.equal(bot.username, botsResult.username); + }); + + it('createBot', async () => { + const bot = TestHelper.fakeBot(); + nock(Client4.getBaseRoute()). + post('/bots'). + reply(200, bot); + await store.dispatch(BotActions.createBot(bot)); + + const state = store.getState(); + const botsResult = state.entities.bots.accounts[bot.user_id]; + assert.equal(bot.username, botsResult.username); + }); + + it('patchBot', async () => { + const bot = TestHelper.fakeBot(); + nock(Client4.getBaseRoute()). + post('/bots'). + reply(200, bot); + await store.dispatch(BotActions.createBot(bot)); + + bot.username = 'mynewusername'; + + nock(Client4.getBaseRoute()). + put(`/bots/${bot.user_id}`). + reply(200, bot); + await store.dispatch(BotActions.patchBot(bot.user_id, bot)); + + const state = store.getState(); + const botsResult = state.entities.bots.accounts[bot.user_id]; + assert.equal(bot.username, botsResult.username); + }); + + it('disableBot', async () => { + const bot = TestHelper.fakeBot(); + nock(Client4.getBaseRoute()). + post('/bots'). + reply(200, bot); + await store.dispatch(BotActions.createBot(bot)); + + // Disable the bot by setting delete_at to a value > 0 + bot.delete_at = 1507840900065; + nock(Client4.getBotRoute(bot.user_id)). + post('/disable'). + reply(200, bot); + await store.dispatch(BotActions.disableBot(bot.user_id)); + + const state = store.getState(); + const botsResult = state.entities.bots.accounts[bot.user_id]; + assert.equal(bot.delete_at, botsResult.delete_at); + + bot.delete_at = 0; + nock(Client4.getBotRoute(bot.user_id)). + post('/enable'). + reply(200, bot); + await store.dispatch(BotActions.enableBot(bot.user_id)); + + const state2 = store.getState(); + const botsResult2 = state2.entities.bots.accounts[bot.user_id]; + assert.equal(bot.delete_at, botsResult2.delete_at); + }); + + it('assignBot', async () => { + const bot = TestHelper.fakeBot(); + nock(Client4.getBaseRoute()). + post('/bots'). + reply(200, bot); + await store.dispatch(BotActions.createBot(bot)); + + bot.owner_id = TestHelper.generateId(); + nock(Client4.getBotRoute(bot.user_id)). + post('/assign/' + bot.owner_id). + reply(200, bot); + await store.dispatch(BotActions.assignBot(bot.user_id, bot.owner_id)); + + const state = store.getState(); + const botsResult = state.entities.bots.accounts[bot.user_id]; + assert.equal(bot.owner_id, botsResult.owner_id); + }); + + it('logout', async () => { + // Fill redux store with somthing + const bot = TestHelper.fakeBot(); + nock(Client4.getBaseRoute()). + post('/bots'). + reply(200, bot); + await store.dispatch(BotActions.createBot(bot)); + + // Should be cleared by logout + nock(Client4.getUsersRoute()). + post('/logout'). + reply(200, {status: 'OK'}); + await store.dispatch(UserActions.logout()); + + // Check is clear + const state = store.getState(); + assert.equal(0, Object.keys(state.entities.bots.accounts).length); + }); +}); diff --git a/app/mm-redux/actions/bots.ts b/app/mm-redux/actions/bots.ts new file mode 100644 index 000000000..cb357711d --- /dev/null +++ b/app/mm-redux/actions/bots.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4} from '@mm-redux/client'; +import {BotTypes} from '@mm-redux/action_types'; +import {bindClientFunc} from './helpers'; + +import {ActionFunc} from '@mm-redux/types/actions'; +import {Bot, BotPatch} from '@mm-redux/types/bots'; + +const BOTS_PER_PAGE_DEFAULT = 20; + +export function createBot(bot: Bot): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createBot, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNT, + params: [ + bot, + ], + }); +} + +export function patchBot(botUserId: string, botPatch: BotPatch): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.patchBot, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNT, + params: [ + botUserId, + botPatch, + ], + }); +} + +export function loadBot(botUserId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getBot, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNT, + params: [ + botUserId, + ], + }); +} + +export function loadBots(page = 0, perPage = BOTS_PER_PAGE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getBotsIncludeDeleted, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNTS, + params: [ + page, + perPage, + ], + }); +} + +export function disableBot(botUserId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.disableBot, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNT, + params: [ + botUserId, + ], + }); +} + +export function enableBot(botUserId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.enableBot, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNT, + params: [ + botUserId, + ], + }); +} + +export function assignBot(botUserId: string, newOwnerId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.assignBot, + onSuccess: BotTypes.RECEIVED_BOT_ACCOUNT, + params: [ + botUserId, + newOwnerId, + ], + }); +} diff --git a/app/mm-redux/actions/channel_categories.ts b/app/mm-redux/actions/channel_categories.ts new file mode 100644 index 000000000..7f82c7f9c --- /dev/null +++ b/app/mm-redux/actions/channel_categories.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ChannelCategoryTypes} from '@mm-redux/action_types'; + +export function expandCategory(categoryId: string) { + return { + type: ChannelCategoryTypes.CATEGORY_EXPANDED, + data: categoryId, + }; +} + +export function collapseCategory(categoryId: string) { + return { + type: ChannelCategoryTypes.CATEGORY_COLLAPSED, + data: categoryId, + }; +} diff --git a/app/mm-redux/actions/channels.test.js b/app/mm-redux/actions/channels.test.js new file mode 100644 index 000000000..1febe3b97 --- /dev/null +++ b/app/mm-redux/actions/channels.test.js @@ -0,0 +1,2330 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/channels'; +import {addUserToTeam} from '@mm-redux/actions/teams'; +import {getProfilesByIds, login} from '@mm-redux/actions/users'; +import {createIncomingHook, createOutgoingHook} from '@mm-redux/actions/integrations'; +import {Client4} from '@mm-redux/client'; +import {General, RequestStatus, Preferences, Permissions} from '../constants'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Channels', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('selectChannel', async () => { + const channelId = TestHelper.generateId(); + + await store.dispatch(Actions.selectChannel(channelId)); + await TestHelper.wait(100); + const state = store.getState(); + + assert.equal(state.entities.channels.currentChannelId, channelId); + }); + + it('createChannel', async () => { + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + await store.dispatch(Actions.createChannel(TestHelper.fakeChannel(TestHelper.basicTeam.id), TestHelper.basicUser.id)); + + const createRequest = store.getState().requests.channels.createChannel; + + if (createRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(createRequest.error)); + } + + const {channels, myMembers} = store.getState().entities.channels; + const channelsCount = Object.keys(channels).length; + const membersCount = Object.keys(myMembers).length; + assert.ok(channels); + assert.ok(myMembers); + assert.ok(channels[Object.keys(myMembers)[0]]); + assert.ok(myMembers[Object.keys(channels)[0]]); + assert.equal(myMembers[Object.keys(channels)[0]].user_id, TestHelper.basicUser.id); + assert.equal(channelsCount, membersCount); + assert.equal(channelsCount, 1); + assert.equal(membersCount, 1); + }); + + it('createDirectChannel', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/ids'). + reply(200, [user]); + + await store.dispatch(getProfilesByIds([user.id])); + + nock(Client4.getBaseRoute()). + post('/channels/direct'). + reply(201, {...TestHelper.fakeChannelWithId(), type: 'D'}); + + const {data: created} = await store.dispatch(Actions.createDirectChannel(TestHelper.basicUser.id, user.id)); + + const createRequest = store.getState().requests.channels.createChannel; + if (createRequest.status === RequestStatus.FAILURE) { + throw new Error(createRequest.error); + } + + const state = store.getState(); + const {channels, myMembers} = state.entities.channels; + const {profiles, profilesInChannel} = state.entities.users; + const preferences = state.entities.preferences.myPreferences; + const channelsCount = Object.keys(channels).length; + const membersCount = Object.keys(myMembers).length; + + assert.ok(channels, 'channels is empty'); + assert.ok(myMembers, 'members is empty'); + assert.ok(profiles[user.id], 'profiles does not have userId'); + assert.ok(Object.keys(preferences).length, 'preferences is empty'); + assert.ok(channels[Object.keys(myMembers)[0]], 'channels should have the member'); + assert.ok(myMembers[Object.keys(channels)[0]], 'members should belong to channel'); + assert.equal(myMembers[Object.keys(channels)[0]].user_id, TestHelper.basicUser.id); + assert.equal(channelsCount, membersCount); + assert.equal(channels[Object.keys(channels)[0]].type, 'D'); + assert.equal(channelsCount, 1); + assert.equal(membersCount, 1); + + assert.ok(profilesInChannel, 'profiles in channel is empty'); + assert.ok(profilesInChannel[created.id], 'profiles in channel is empty for channel'); + assert.equal(profilesInChannel[created.id].size, 2, 'incorrect number of profiles in channel'); + assert.ok(profilesInChannel[created.id].has(TestHelper.basicUser.id), 'creator is not in channel'); + assert.ok(profilesInChannel[created.id].has(user.id), 'user is not in channel'); + }); + + it('createGroupChannel', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user2 = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + TestHelper.mockLogin(); + await store.dispatch(login(TestHelper.basicUser.email, TestHelper.basicUser.password)); + + nock(Client4.getBaseRoute()). + post('/users/ids'). + reply(200, [user, user2]); + + await store.dispatch(getProfilesByIds([user.id, user2.id])); + + nock(Client4.getBaseRoute()). + post('/channels/group'). + reply(201, {...TestHelper.fakeChannelWithId(), type: 'G'}); + + const result = await store.dispatch(Actions.createGroupChannel([TestHelper.basicUser.id, user.id, user2.id])); + const created = result.data; + + assert.ok(!result.error, 'error was returned'); + assert.ok(created, 'channel was not returned'); + + const createRequest = store.getState().requests.channels.createChannel; + if (createRequest.status === RequestStatus.FAILURE) { + throw new Error(createRequest.error); + } + + const state = store.getState(); + const {channels, myMembers} = state.entities.channels; + const preferences = state.entities.preferences.myPreferences; + const {profilesInChannel} = state.entities.users; + + assert.ok(channels, 'channels is empty'); + assert.ok(channels[created.id], 'channel does not exist'); + assert.ok(myMembers, 'members is empty'); + assert.ok(myMembers[created.id], 'member does not exist'); + assert.ok(Object.keys(preferences).length, 'preferences is empty'); + + assert.ok(profilesInChannel, 'profiles in channel is empty'); + assert.ok(profilesInChannel[created.id], 'profiles in channel is empty for channel'); + assert.equal(profilesInChannel[created.id].size, 3, 'incorrect number of profiles in channel'); + assert.ok(profilesInChannel[created.id].has(TestHelper.basicUser.id), 'creator is not in channel'); + assert.ok(profilesInChannel[created.id].has(user.id), 'user is not in channel'); + assert.ok(profilesInChannel[created.id].has(user2.id), 'user2 is not in channel'); + }); + + it('updateChannel', async () => { + const channel = { + ...TestHelper.basicChannel, + purpose: 'This is to test redux', + header: 'MM with Redux', + }; + + nock(Client4.getBaseRoute()). + put(`/channels/${channel.id}`). + reply(200, channel); + + await store.dispatch(Actions.updateChannel(channel)); + + const updateRequest = store.getState().requests.channels.updateChannel; + if (updateRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(updateRequest.error)); + } + + const {channels} = store.getState().entities.channels; + const channelId = Object.keys(channels)[0]; + assert.ok(channelId); + assert.ok(channels[channelId]); + assert.strictEqual(channels[channelId].header, 'MM with Redux'); + }); + + it('patchChannel', async () => { + const channel = { + header: 'MM with Redux2', + }; + + nock(Client4.getBaseRoute()). + put(`/channels/${TestHelper.basicChannel.id}/patch`). + reply(200, {...TestHelper.basicChannel, ...channel}); + + await store.dispatch(Actions.patchChannel(TestHelper.basicChannel.id, channel)); + + const updateRequest = store.getState().requests.channels.updateChannel; + if (updateRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(updateRequest.error)); + } + + const {channels} = store.getState().entities.channels; + const channelId = Object.keys(channels)[0]; + assert.ok(channelId); + assert.ok(channels[channelId]); + assert.strictEqual(channels[channelId].header, 'MM with Redux2'); + }); + + it('updateChannelPrivacy', async () => { + const publicChannel = TestHelper.basicChannel; + nock(Client4.getChannelRoute(publicChannel.id)). + put('/privacy'). + reply(200, {...publicChannel, type: General.PRIVATE_CHANNEL}); + + assert.equal(publicChannel.type, General.OPEN_CHANNEL); + + await store.dispatch(Actions.updateChannelPrivacy(publicChannel.id, General.PRIVATE_CHANNEL)); + + const updateRequest = store.getState().requests.channels.updateChannel; + if (updateRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(updateRequest.error)); + } + + const {channels} = store.getState().entities.channels; + const channelId = Object.keys(channels)[0]; + assert.ok(channelId); + assert.ok(channels[channelId]); + assert.equal(channels[channelId].type, General.PRIVATE_CHANNEL); + }); + + it('convertChannelToPrivate', async () => { + const publicChannel = TestHelper.basicChannel; + nock(Client4.getChannelRoute(publicChannel.id)). + post('/convert'). + reply(200, {...TestHelper.basicChannel, type: General.PRIVATE_CHANNEL}); + + assert.equal(TestHelper.basicChannel.type, General.OPEN_CHANNEL); + + await store.dispatch(Actions.convertChannelToPrivate(TestHelper.basicChannel.id)); + + const updateRequest = store.getState().requests.channels.updateChannel; + if (updateRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(updateRequest.error)); + } + + const {channels} = store.getState().entities.channels; + const channelId = Object.keys(channels)[0]; + assert.ok(channelId); + assert.ok(channels[channelId]); + assert.equal(channels[channelId].type, General.PRIVATE_CHANNEL); + }); + + it('getChannel', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}`). + reply(200, TestHelper.basicChannel); + + await store.dispatch(Actions.getChannel(TestHelper.basicChannel.id)); + + const {channels} = store.getState().entities.channels; + assert.ok(channels[TestHelper.basicChannel.id]); + }); + + it('getChannelByNameAndTeamName', async () => { + nock(Client4.getTeamsRoute()). + get(`/name/${TestHelper.basicTeam.name}/channels/name/${TestHelper.basicChannel.name}?include_deleted=false`). + reply(200, TestHelper.basicChannel); + + await store.dispatch(Actions.getChannelByNameAndTeamName(TestHelper.basicTeam.name, TestHelper.basicChannel.name)); + + const {channels} = store.getState().entities.channels; + assert.ok(channels[TestHelper.basicChannel.id]); + }); + + it('getChannelAndMyMember', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}`). + reply(200, TestHelper.basicChannel); + + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/members/me`). + reply(200, TestHelper.basicChannelMember); + + await store.dispatch(Actions.getChannelAndMyMember(TestHelper.basicChannel.id)); + + const {channels, myMembers} = store.getState().entities.channels; + assert.ok(channels[TestHelper.basicChannel.id]); + assert.ok(myMembers[TestHelper.basicChannel.id]); + }); + + it('fetchMyChannelsAndMembers', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/channels/direct'). + reply(201, {...TestHelper.fakeChannelWithId(), team_id: '', type: 'D'}); + + const {data: directChannel} = await store.dispatch(Actions.createDirectChannel(TestHelper.basicUser.id, user.id)); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels`). + query(true). + reply(200, [directChannel, TestHelper.basicChannel]); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels/members`). + reply(200, [{user_id: TestHelper.basicUser.id, roles: 'channel_user', channel_id: directChannel.id}, TestHelper.basicChannelMember]); + + await store.dispatch(Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)); + + const {channels, channelsInTeam, myMembers} = store.getState().entities.channels; + assert.ok(channels); + assert.ok(myMembers); + assert.ok(channels[Object.keys(myMembers)[0]]); + assert.ok(myMembers[Object.keys(channels)[0]]); + assert.ok(channelsInTeam[''].has(directChannel.id)); + assert.equal(Object.keys(channels).length, Object.keys(myMembers).length); + }); + + it('updateChannelNotifyProps', async () => { + const notifyProps = { + mark_unread: 'mention', + desktop: 'none', + }; + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels`). + query(true). + reply(200, [TestHelper.basicChannel]); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels/members`). + reply(200, [TestHelper.basicChannelMember]); + + await store.dispatch(Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)); + + nock(Client4.getBaseRoute()). + put(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}/notify_props`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.updateChannelNotifyProps( + TestHelper.basicUser.id, + TestHelper.basicChannel.id, + notifyProps)); + + const members = store.getState().entities.channels.myMembers; + const member = members[TestHelper.basicChannel.id]; + assert.ok(member); + assert.equal(member.notify_props.mark_unread, 'mention'); + assert.equal(member.notify_props.desktop, 'none'); + }); + + it('deleteChannel', async () => { + const secondClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + await secondClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + const secondChannel = await secondClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id)); + + nock(Client4.getBaseRoute()). + post(`/channels/${secondChannel.id}/members`). + reply(201, {user_id: TestHelper.basicUser.id, roles: 'channel_user', channel_id: secondChannel.id}); + + await store.dispatch(Actions.joinChannel( + TestHelper.basicUser.id, + TestHelper.basicTeam.id, + secondChannel.id, + )); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels`). + query(true). + reply(200, [secondChannel, TestHelper.basicChannel]); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels/members`). + reply(200, [{user_id: TestHelper.basicUser.id, roles: 'channel_user', channel_id: secondChannel.id}, TestHelper.basicChannelMember]); + + await store.dispatch(Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)); + + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, { + id: TestHelper.generateId(), + create_at: 1507840900004, + update_at: 1507840900004, + delete_at: 0, + user_id: TestHelper.basicUser.id, + channel_id: secondChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'TestIncomingHook', + description: 'Some description.', + }); + const incomingHook = await store.dispatch(createIncomingHook({channel_id: secondChannel.id, display_name: 'test', description: 'test'})); + + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, { + id: TestHelper.generateId(), + token: TestHelper.generateId(), + create_at: 1507841118796, + update_at: 1507841118796, + delete_at: 0, + creator_id: TestHelper.basicUser.id, + channel_id: secondChannel.id, + team_id: TestHelper.basicTeam.id, + trigger_words: ['testword'], + trigger_when: 0, + callback_urls: ['http://notarealurl'], + display_name: 'TestOutgoingHook', + description: '', + content_type: 'application/x-www-form-urlencoded', + }); + const outgoingHook = await store.dispatch(createOutgoingHook({ + channel_id: secondChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'TestOutgoingHook', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://notarealurl']}, + )); + + nock(Client4.getBaseRoute()). + delete(`/channels/${secondChannel.id}`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.deleteChannel(secondChannel.id)); + + const {incomingHooks, outgoingHooks} = store.getState().entities.integrations; + + assert.ifError(incomingHooks[incomingHook.id]); + assert.ifError(outgoingHooks[outgoingHook.id]); + }); + + it('unarchiveChannel', async () => { + const secondClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + await secondClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + const secondChannel = await secondClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id)); + + nock(Client4.getBaseRoute()). + post(`/channels/${secondChannel.id}/members`). + reply(201, {user_id: TestHelper.basicUser.id, roles: 'channel_user', channel_id: secondChannel.id}); + + await store.dispatch(Actions.joinChannel( + TestHelper.basicUser.id, + TestHelper.basicTeam.id, + secondChannel.id, + )); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels`). + query(true). + reply(200, [secondChannel, TestHelper.basicChannel]); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels/members`). + reply(200, [{user_id: TestHelper.basicUser.id, roles: 'channel_user', channel_id: secondChannel.id}, TestHelper.basicChannelMember]); + + await store.dispatch(Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)); + + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, { + id: TestHelper.generateId(), + create_at: 1507840900004, + update_at: 1507840900004, + delete_at: 1609090954545, + user_id: TestHelper.basicUser.id, + channel_id: secondChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'TestIncomingHook', + description: 'Some description.', + }); + const incomingHook = await store.dispatch(createIncomingHook({channel_id: secondChannel.id, display_name: 'test', description: 'test'})); + + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, { + id: TestHelper.generateId(), + token: TestHelper.generateId(), + create_at: 1507841118796, + update_at: 1507841118796, + delete_at: 1609090954545, + creator_id: TestHelper.basicUser.id, + channel_id: secondChannel.id, + team_id: TestHelper.basicTeam.id, + trigger_words: ['testword'], + trigger_when: 0, + callback_urls: ['http://notarealurl'], + display_name: 'TestOutgoingHook', + description: '', + content_type: 'application/x-www-form-urlencoded', + }); + const outgoingHook = await store.dispatch(createOutgoingHook({ + channel_id: secondChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'TestOutgoingHook', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://notarealurl']}, + )); + + nock(Client4.getBaseRoute()). + delete(`/channels/${secondChannel.id}`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.unarchiveChannel(secondChannel.id)); + + const {incomingHooks, outgoingHooks} = store.getState().entities.integrations; + + assert.ifError(incomingHooks[incomingHook.id]); + assert.ifError(outgoingHooks[outgoingHook.id]); + }); + + describe('viewChannel', () => { + test('should contact server and update last_viewed_at of both channels', async () => { + const channelId = TestHelper.generateId(); + const prevChannelId = TestHelper.generateId(); + + const currentUserId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + myMembers: { + [channelId]: { + channel_id: channelId, + last_viewed_at: 1000, + roles: '', + }, + [prevChannelId]: { + channel_id: prevChannelId, + last_viewed_at: 1000, + roles: '', + }, + }, + }, + users: { + currentUserId, + }, + }, + }); + + nock(Client4.getBaseRoute()). + post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: prevChannelId}). + reply(200, OK_RESPONSE); + + const now = Date.now(); + + const result = await store.dispatch(Actions.viewChannel(channelId, prevChannelId)); + expect(result).toEqual({data: true}); + + const state = store.getState(); + expect(state.entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(now); + expect(state.entities.channels.myMembers[prevChannelId].last_viewed_at).toBeGreaterThan(now); + }); + + test('should clear manually unread state from current channel', async () => { + const channelId = TestHelper.generateId(); + + const currentUserId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + manuallyUnread: { + [channelId]: true, + }, + myMembers: { + [channelId]: { + channel_id: channelId, + last_viewed_at: 1000, + roles: '', + }, + }, + }, + users: { + currentUserId, + }, + }, + }); + + nock(Client4.getBaseRoute()). + post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: ''}). + reply(200, OK_RESPONSE); + + const result = await store.dispatch(Actions.viewChannel(channelId)); + expect(result).toEqual({data: true}); + + const state = store.getState(); + expect(state.entities.channels.manuallyUnread[channelId]).not.toBe(true); + }); + + test('should not update last_viewed_at of previous channel if it is manually marked as unread', async () => { + const channelId = TestHelper.generateId(); + const prevChannelId = TestHelper.generateId(); + + const currentUserId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + manuallyUnread: { + [prevChannelId]: true, + }, + myMembers: { + [channelId]: { + channel_id: channelId, + last_viewed_at: 1000, + roles: '', + }, + [prevChannelId]: { + channel_id: prevChannelId, + last_viewed_at: 1000, + roles: '', + }, + }, + }, + users: { + currentUserId, + }, + }, + }); + + nock(Client4.getBaseRoute()). + post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: ''}). + reply(200, OK_RESPONSE); + + const now = Date.now(); + + const result = await store.dispatch(Actions.viewChannel(channelId, prevChannelId)); + expect(result).toEqual({data: true}); + + const state = store.getState(); + expect(state.entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(now); + expect(state.entities.channels.myMembers[prevChannelId].last_viewed_at).toBe(1000); + }); + }); + + it('markChannelAsViewed', async () => { + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await Client4.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels`). + query(true). + reply(200, [userChannel, TestHelper.basicChannel]); + + nock(Client4.getBaseRoute()). + get(`/users/me/teams/${TestHelper.basicTeam.id}/channels/members`). + reply(200, [{user_id: TestHelper.basicUser.id, roles: 'channel_user', channel_id: userChannel.id}, TestHelper.basicChannelMember]); + + await store.dispatch(Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)); + + const timestamp = Date.now(); + let members = store.getState().entities.channels.myMembers; + let member = members[TestHelper.basicChannel.id]; + const otherMember = members[userChannel.id]; + assert.ok(member); + assert.ok(otherMember); + + await TestHelper.wait(50); + + await store.dispatch(Actions.markChannelAsViewed(TestHelper.basicChannel.id)); + + members = store.getState().entities.channels.myMembers; + member = members[TestHelper.basicChannel.id]; + assert.ok(member.last_viewed_at > timestamp); + }); + + describe('markChannelAsUnread', () => { + it('plain message', async () => { + const teamId = TestHelper.generateId(); + const channelId = TestHelper.generateId(); + const userId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: {team_id: teamId, total_msg_count: 10}, + }, + myMembers: { + [channelId]: {msg_count: 10, mention_count: 0}, + }, + }, + teams: { + myMembers: { + [teamId]: {msg_count: 0, mention_count: 0}, + }, + }, + users: { + currentUserId: userId, + }, + }, + }); + + store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [TestHelper.generateId()])); + + const state = store.getState(); + assert.equal(state.entities.channels.channels[channelId].total_msg_count, 11); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, 10); + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 1); + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 0); + }); + + it('message mentioning current user', async () => { + const teamId = TestHelper.generateId(); + const channelId = TestHelper.generateId(); + const userId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: {team_id: teamId, total_msg_count: 10}, + }, + myMembers: { + [channelId]: {msg_count: 10, mention_count: 0}, + }, + }, + teams: { + myMembers: { + [teamId]: {msg_count: 0, mention_count: 0}, + }, + }, + users: { + currentUserId: userId, + }, + }, + }); + + store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId])); + + const state = store.getState(); + assert.equal(state.entities.channels.channels[channelId].total_msg_count, 11); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, 10); + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 1); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 1); + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 1); + }); + + it('plain message with mark_unread="mention"', async () => { + const teamId = TestHelper.generateId(); + const channelId = TestHelper.generateId(); + const userId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: {team_id: teamId, total_msg_count: 10}, + }, + myMembers: { + [channelId]: {msg_count: 10, mention_count: 0, notify_props: {mark_unread: General.MENTION}}, + }, + }, + teams: { + myMembers: { + [teamId]: {msg_count: 0, mention_count: 0}, + }, + }, + users: { + currentUserId: userId, + }, + }, + }); + + store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [TestHelper.generateId()])); + + const state = store.getState(); + assert.equal(state.entities.channels.channels[channelId].total_msg_count, 11); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, 11); + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 0); + }); + + it('message mentioning current user with mark_unread="mention"', async () => { + const teamId = TestHelper.generateId(); + const channelId = TestHelper.generateId(); + const userId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: {team_id: teamId, total_msg_count: 10}, + }, + myMembers: { + [channelId]: {msg_count: 10, mention_count: 0, notify_props: {mark_unread: General.MENTION}}, + }, + }, + teams: { + myMembers: { + [teamId]: {msg_count: 0, mention_count: 0}, + }, + }, + users: { + currentUserId: userId, + }, + }, + }); + + store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId])); + + const state = store.getState(); + assert.equal(state.entities.channels.channels[channelId].total_msg_count, 11); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, 11); + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 1); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 1); + }); + }); + + describe('markChannelAsRead', () => { + it('one read channel', async () => { + const channelId = TestHelper.generateId(); + const teamId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: { + id: channelId, + team_id: teamId, + total_msg_count: 10, + }, + }, + myMembers: { + [channelId]: { + channel_id: channelId, + mention_count: 0, + msg_count: 10, + }, + }, + }, + teams: { + myMembers: { + [teamId]: { + id: teamId, + mention_count: 0, + msg_count: 0, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, state.entities.channels.channels[channelId].total_msg_count); + + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 0); + }); + + it('one unread channel', async () => { + const channelId = TestHelper.generateId(); + const teamId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: { + id: channelId, + team_id: teamId, + total_msg_count: 10, + }, + }, + myMembers: { + [channelId]: { + channel_id: channelId, + mention_count: 2, + msg_count: 5, + }, + }, + }, + teams: { + myMembers: { + [teamId]: { + id: teamId, + mention_count: 2, + msg_count: 5, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, state.entities.channels.channels[channelId].total_msg_count); + + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 0); + }); + + it('one unread DM channel', async () => { + const channelId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId]: { + id: channelId, + team_id: '', + total_msg_count: 10, + }, + }, + myMembers: { + [channelId]: { + channel_id: channelId, + mention_count: 2, + msg_count: 5, + }, + }, + }, + teams: { + myMembers: { + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId].msg_count, state.entities.channels.channels[channelId].total_msg_count); + }); + + it('two unread channels, same team, reading one', async () => { + const channelId1 = TestHelper.generateId(); + const channelId2 = TestHelper.generateId(); + const teamId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId1]: { + id: channelId1, + team_id: teamId, + total_msg_count: 10, + }, + [channelId2]: { + id: channelId2, + team_id: teamId, + total_msg_count: 12, + }, + }, + myMembers: { + [channelId1]: { + channel_id: channelId1, + mention_count: 2, + msg_count: 5, + }, + [channelId2]: { + channel_id: channelId2, + mention_count: 4, + msg_count: 9, + }, + }, + }, + teams: { + myMembers: { + [teamId]: { + id: teamId, + mention_count: 6, + msg_count: 8, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId1)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId1].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId1].msg_count, state.entities.channels.channels[channelId1].total_msg_count); + + assert.equal(state.entities.channels.myMembers[channelId2].mention_count, 4); + assert.equal(state.entities.channels.myMembers[channelId2].msg_count, 9); + + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 4); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 3); + }); + + it('two unread channels, same team, reading both', async () => { + const channelId1 = TestHelper.generateId(); + const channelId2 = TestHelper.generateId(); + const teamId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId1]: { + id: channelId1, + team_id: teamId, + total_msg_count: 10, + }, + [channelId2]: { + id: channelId2, + team_id: teamId, + total_msg_count: 12, + }, + }, + myMembers: { + [channelId1]: { + channel_id: channelId1, + mention_count: 2, + msg_count: 5, + }, + [channelId2]: { + channel_id: channelId2, + mention_count: 4, + msg_count: 9, + }, + }, + }, + teams: { + myMembers: { + [teamId]: { + id: teamId, + mention_count: 6, + msg_count: 8, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId1].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId1].msg_count, state.entities.channels.channels[channelId1].total_msg_count); + + assert.equal(state.entities.channels.myMembers[channelId2].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId2].msg_count, state.entities.channels.channels[channelId2].total_msg_count); + + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 0); + }); + + it('two unread channels, same team, reading both (opposite order)', async () => { + const channelId1 = TestHelper.generateId(); + const channelId2 = TestHelper.generateId(); + const teamId = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId1]: { + id: channelId1, + team_id: teamId, + total_msg_count: 10, + }, + [channelId2]: { + id: channelId2, + team_id: teamId, + total_msg_count: 12, + }, + }, + myMembers: { + [channelId1]: { + channel_id: channelId1, + mention_count: 2, + msg_count: 5, + }, + [channelId2]: { + channel_id: channelId2, + mention_count: 4, + msg_count: 9, + }, + }, + }, + teams: { + myMembers: { + [teamId]: { + id: teamId, + mention_count: 6, + msg_count: 8, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId2, channelId1)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId1].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId1].msg_count, state.entities.channels.channels[channelId1].total_msg_count); + + assert.equal(state.entities.channels.myMembers[channelId2].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId2].msg_count, state.entities.channels.channels[channelId2].total_msg_count); + + assert.equal(state.entities.teams.myMembers[teamId].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId].msg_count, 0); + }); + + it('two unread channels, different teams, reading one', async () => { + const channelId1 = TestHelper.generateId(); + const channelId2 = TestHelper.generateId(); + const teamId1 = TestHelper.generateId(); + const teamId2 = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId1]: { + id: channelId1, + team_id: teamId1, + total_msg_count: 10, + }, + [channelId2]: { + id: channelId2, + team_id: teamId2, + total_msg_count: 12, + }, + }, + myMembers: { + [channelId1]: { + channel_id: channelId1, + mention_count: 2, + msg_count: 5, + }, + [channelId2]: { + channel_id: channelId2, + mention_count: 4, + msg_count: 9, + }, + }, + }, + teams: { + myMembers: { + [teamId1]: { + id: teamId1, + mention_count: 2, + msg_count: 5, + }, + [teamId2]: { + id: teamId2, + mention_count: 4, + msg_count: 3, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId1)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId1].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId1].msg_count, state.entities.channels.channels[channelId1].total_msg_count); + + assert.equal(state.entities.channels.myMembers[channelId2].mention_count, 4); + assert.equal(state.entities.channels.myMembers[channelId2].msg_count, 9); + + assert.equal(state.entities.teams.myMembers[teamId1].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId1].msg_count, 0); + + assert.equal(state.entities.teams.myMembers[teamId2].mention_count, 4); + assert.equal(state.entities.teams.myMembers[teamId2].msg_count, 3); + }); + + it('two unread channels, different teams, reading both', async () => { + const channelId1 = TestHelper.generateId(); + const channelId2 = TestHelper.generateId(); + const teamId1 = TestHelper.generateId(); + const teamId2 = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId1]: { + id: channelId1, + team_id: teamId1, + total_msg_count: 10, + }, + [channelId2]: { + id: channelId2, + team_id: teamId2, + total_msg_count: 12, + }, + }, + myMembers: { + [channelId1]: { + channel_id: channelId1, + mention_count: 2, + msg_count: 5, + }, + [channelId2]: { + channel_id: channelId2, + mention_count: 4, + msg_count: 9, + }, + }, + }, + teams: { + myMembers: { + [teamId1]: { + id: teamId1, + mention_count: 2, + msg_count: 5, + }, + [teamId2]: { + id: teamId2, + mention_count: 4, + msg_count: 3, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId1].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId1].msg_count, state.entities.channels.channels[channelId1].total_msg_count); + + assert.equal(state.entities.channels.myMembers[channelId2].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId2].msg_count, state.entities.channels.channels[channelId2].total_msg_count); + + assert.equal(state.entities.teams.myMembers[teamId1].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId1].msg_count, 0); + + assert.equal(state.entities.teams.myMembers[teamId2].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId2].msg_count, 0); + }); + + it('two unread channels, different teams, reading both (opposite order)', async () => { + const channelId1 = TestHelper.generateId(); + const channelId2 = TestHelper.generateId(); + const teamId1 = TestHelper.generateId(); + const teamId2 = TestHelper.generateId(); + + store = await configureStore({ + entities: { + channels: { + channels: { + [channelId1]: { + id: channelId1, + team_id: teamId1, + total_msg_count: 10, + }, + [channelId2]: { + id: channelId2, + team_id: teamId2, + total_msg_count: 12, + }, + }, + myMembers: { + [channelId1]: { + channel_id: channelId1, + mention_count: 2, + msg_count: 5, + }, + [channelId2]: { + channel_id: channelId2, + mention_count: 4, + msg_count: 9, + }, + }, + }, + teams: { + myMembers: { + [teamId1]: { + id: teamId1, + mention_count: 2, + msg_count: 5, + }, + [teamId2]: { + id: teamId2, + mention_count: 4, + msg_count: 3, + }, + }, + }, + }, + }); + + await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2)); + + const state = store.getState(); + + assert.equal(state.entities.channels.myMembers[channelId1].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId1].msg_count, state.entities.channels.channels[channelId1].total_msg_count); + + assert.equal(state.entities.channels.myMembers[channelId2].mention_count, 0); + assert.equal(state.entities.channels.myMembers[channelId2].msg_count, state.entities.channels.channels[channelId2].total_msg_count); + + assert.equal(state.entities.teams.myMembers[teamId1].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId1].msg_count, 0); + + assert.equal(state.entities.teams.myMembers[teamId2].mention_count, 0); + assert.equal(state.entities.teams.myMembers[teamId2].msg_count, 0); + }); + }); + + it('getChannels', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await userClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + nock(Client4.getTeamsRoute()). + get(`/${TestHelper.basicTeam.id}/channels`). + query(true). + reply(200, [TestHelper.basicChannel, userChannel]); + + await store.dispatch(Actions.getChannels(TestHelper.basicTeam.id, 0)); + + const moreRequest = store.getState().requests.channels.getChannels; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + const {channels, channelsInTeam, myMembers} = store.getState().entities.channels; + const channel = channels[userChannel.id]; + const team = channelsInTeam[userChannel.team_id]; + + assert.ok(channel); + assert.ok(team); + assert.ok(team.has(userChannel.id)); + assert.ifError(myMembers[channel.id]); + }); + + it('getArchivedChannels', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await userClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + nock(Client4.getTeamsRoute()). + get(`/${TestHelper.basicTeam.id}/channels/deleted`). + query(true). + reply(200, [TestHelper.basicChannel, userChannel]); + + await store.dispatch(Actions.getArchivedChannels(TestHelper.basicTeam.id, 0)); + + const moreRequest = store.getState().requests.channels.getChannels; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + const {channels, channelsInTeam, myMembers} = store.getState().entities.channels; + const channel = channels[userChannel.id]; + const team = channelsInTeam[userChannel.team_id]; + + assert.ok(channel); + assert.ok(team); + assert.ok(team.has(userChannel.id)); + assert.ifError(myMembers[channel.id]); + }); + + it('getAllChannels', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await userClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + nock(Client4.getBaseRoute()). + get('/channels'). + query(true). + reply(200, [TestHelper.basicChannel, userChannel]); + + const {data} = await store.dispatch(Actions.getAllChannels(0)); + + const moreRequest = store.getState().requests.channels.getAllChannels; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + assert.ok(data.length === 2); + }); + + it('getAllChannelsWithCount', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await userClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + const mockTotalCount = 84; + nock(Client4.getBaseRoute()). + get('/channels'). + query(true). + reply(200, {channels: [TestHelper.basicChannel, userChannel], total_count: mockTotalCount}); + + assert.ok(store.getState().entities.channels.totalCount === 0); + + const {data} = await store.dispatch(Actions.getAllChannelsWithCount(0)); + + const moreRequest = store.getState().requests.channels.getAllChannels; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + assert.ok(data.channels.length === 2); + assert.ok(data.total_count === mockTotalCount); + + assert.ok(store.getState().entities.channels.totalCount === mockTotalCount); + }); + + it('searchAllChannels', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await userClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + nock(Client4.getBaseRoute()). + post('/channels/search'). + reply(200, [TestHelper.basicChannel, userChannel]); + + const {data} = await store.dispatch(Actions.searchAllChannels('test', 0)); + + const moreRequest = store.getState().requests.channels.getAllChannels; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + nock(Client4.getBaseRoute()). + post('/channels/search'). + reply(200, {channels: [TestHelper.basicChannel, userChannel], total_count: 2}); + + const response = await store.dispatch(Actions.searchAllChannels('test', '', false, 0, 100)); + + const paginatedRequest = store.getState().requests.channels.getAllChannels; + if (paginatedRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(paginatedRequest.error)); + } + + assert.ok(response.data.channels.length === 2); + }); + + it('searchArchivedChannels', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userChannel = await userClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id), + ); + + nock(Client4.getTeamsRoute()). + post(`/${TestHelper.basicTeam.id}/channels/search_archived`). + reply(200, [TestHelper.basicChannel, userChannel]); + + const {data} = await store.dispatch(Actions.searchChannels(TestHelper.basicTeam.id, 'test', true)); + + const moreRequest = store.getState().requests.channels.getChannels; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + assert.ok(data.length === 2); + }); + + it('getChannelMembers', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/members`). + query(true). + reply(200, [TestHelper.basicChannelMember]); + + await store.dispatch(Actions.getChannelMembers(TestHelper.basicChannel.id)); + + const {membersInChannel} = store.getState().entities.channels; + + assert.ok(membersInChannel); + assert.ok(membersInChannel[TestHelper.basicChannel.id]); + assert.ok(membersInChannel[TestHelper.basicChannel.id][TestHelper.basicUser.id]); + }); + + it('getChannelMember', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`). + reply(200, TestHelper.basicChannelMember); + + await store.dispatch(Actions.getChannelMember(TestHelper.basicChannel.id, TestHelper.basicUser.id)); + + const {membersInChannel} = store.getState().entities.channels; + + assert.ok(membersInChannel); + assert.ok(membersInChannel[TestHelper.basicChannel.id]); + assert.ok(membersInChannel[TestHelper.basicChannel.id][TestHelper.basicUser.id]); + }); + + it('getMyChannelMember', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/members/me`). + reply(200, TestHelper.basicChannelMember); + + await store.dispatch(Actions.getMyChannelMember(TestHelper.basicChannel.id)); + + const {myMembers} = store.getState().entities.channels; + + assert.ok(myMembers); + assert.ok(myMembers[TestHelper.basicChannel.id]); + }); + + it('getChannelMembersByIds', async () => { + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members/ids`). + reply(200, [TestHelper.basicChannelMember]); + + await store.dispatch(Actions.getChannelMembersByIds(TestHelper.basicChannel.id, [TestHelper.basicUser.id])); + + const {membersInChannel} = store.getState().entities.channels; + + assert.ok(membersInChannel); + assert.ok(membersInChannel[TestHelper.basicChannel.id]); + assert.ok(membersInChannel[TestHelper.basicChannel.id][TestHelper.basicUser.id]); + }); + + it('getChannelStats', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/stats`). + reply(200, {channel_id: TestHelper.basicChannel.id, member_count: 1}); + + await store.dispatch(Actions.getChannelStats(TestHelper.basicChannel.id)); + + const {stats} = store.getState().entities.channels; + const stat = stats[TestHelper.basicChannel.id]; + assert.ok(stat); + assert.ok(stat.member_count >= 1); + }); + + it('addChannelMember', async () => { + const channelId = TestHelper.basicChannel.id; + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id}); + + await store.dispatch(Actions.joinChannel(TestHelper.basicUser.id, TestHelper.basicTeam.id, channelId)); + + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/stats`). + reply(200, {channel_id: TestHelper.basicChannel.id, member_count: 1}); + + await store.dispatch(Actions.getChannelStats(channelId)); + + let state = store.getState(); + let {stats} = state.entities.channels; + assert.ok(stats, 'stats'); + assert.ok(stats[channelId], 'stats for channel'); + assert.ok(stats[channelId].member_count, 'member count for channel'); + assert.ok(stats[channelId].member_count >= 1, 'incorrect member count for channel'); + + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: user.id}); + + await store.dispatch(Actions.addChannelMember(channelId, user.id)); + + state = store.getState(); + + const {profilesInChannel, profilesNotInChannel} = state.entities.users; + const channel = profilesInChannel[channelId]; + const notChannel = profilesNotInChannel[channelId]; + assert.ok(channel); + assert.ok(notChannel); + assert.ok(channel.has(user.id)); + assert.equal(notChannel.has(user.id), false, 'user should not present in profilesNotInChannel'); + + stats = state.entities.channels.stats; + assert.ok(stats, 'stats'); + assert.ok(stats[channelId], 'stats for channel'); + assert.ok(stats[channelId].member_count, 'member count for channel'); + assert.ok(stats[channelId].member_count >= 2, 'incorrect member count for channel'); + }); + + it('removeChannelMember', async () => { + const channelId = TestHelper.basicChannel.id; + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id}); + + await store.dispatch(Actions.joinChannel(TestHelper.basicUser.id, TestHelper.basicTeam.id, channelId)); + + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/stats`). + reply(200, {channel_id: TestHelper.basicChannel.id, member_count: 1}); + + await store.dispatch(Actions.getChannelStats(channelId)); + + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: user.id}); + + await store.dispatch(Actions.addChannelMember(channelId, user.id)); + + let state = store.getState(); + let {stats} = state.entities.channels; + assert.ok(stats, 'stats'); + assert.ok(stats[channelId], 'stats for channel'); + assert.ok(stats[channelId].member_count, 'member count for channel'); + assert.ok(stats[channelId].member_count >= 2, 'incorrect member count for channel'); + + nock(Client4.getBaseRoute()). + delete(`/channels/${TestHelper.basicChannel.id}/members/${user.id}`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.removeChannelMember(channelId, user.id)); + + state = store.getState(); + + const {profilesInChannel, profilesNotInChannel} = state.entities.users; + const channel = profilesInChannel[channelId]; + const notChannel = profilesNotInChannel[channelId]; + assert.ok(channel); + assert.ok(notChannel); + assert.ok(notChannel.has(user.id)); + assert.equal(channel.has(user.id), false, 'user should not present in profilesInChannel'); + + stats = state.entities.channels.stats; + assert.ok(stats, 'stats'); + assert.ok(stats[channelId], 'stats for channel'); + assert.ok(stats[channelId].member_count, 'member count for channel'); + assert.ok(stats[channelId].member_count >= 1, 'incorrect member count for channel'); + }); + + it('updateChannelMemberRoles', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getTeamsRoute()). + post(`/${TestHelper.basicChannel.id}/members`). + reply(201, {team_id: TestHelper.basicTeam.id, roles: 'channel_user', user_id: user.id}); + + await store.dispatch(addUserToTeam(TestHelper.basicTeam.id, user.id)); + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: user.id}); + + await store.dispatch(Actions.addChannelMember(TestHelper.basicChannel.id, user.id)); + + const roles = General.CHANNEL_USER_ROLE + ' ' + General.CHANNEL_ADMIN_ROLE; + + nock(Client4.getBaseRoute()). + put(`/channels/${TestHelper.basicChannel.id}/members/${user.id}/roles`). + reply(200, {roles}); + await store.dispatch(Actions.updateChannelMemberRoles(TestHelper.basicChannel.id, user.id, roles)); + + const members = store.getState().entities.channels.membersInChannel; + + assert.ok(members[TestHelper.basicChannel.id]); + assert.ok(members[TestHelper.basicChannel.id][user.id]); + assert.ok(members[TestHelper.basicChannel.id][user.id].roles === roles); + }); + + it('updateChannelHeader', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}`). + reply(200, TestHelper.basicChannel); + + await store.dispatch(Actions.getChannel(TestHelper.basicChannel.id)); + + const header = 'this is an updated test header'; + + await store.dispatch(Actions.updateChannelHeader(TestHelper.basicChannel.id, header)); + + const {channels} = store.getState().entities.channels; + const channel = channels[TestHelper.basicChannel.id]; + assert.ok(channel); + assert.deepEqual(channel.header, header); + }); + + it('updateChannelPurpose', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}`). + reply(200, TestHelper.basicChannel); + + await store.dispatch(Actions.getChannel(TestHelper.basicChannel.id)); + + const purpose = 'this is an updated test purpose'; + await store.dispatch(Actions.updateChannelPurpose(TestHelper.basicChannel.id, purpose)); + const {channels} = store.getState().entities.channels; + const channel = channels[TestHelper.basicChannel.id]; + assert.ok(channel); + assert.deepEqual(channel.purpose, purpose); + }); + + it('leaveChannel', (done) => { + async function test() { + TestHelper.mockLogin(); + await store.dispatch(login(TestHelper.basicUser.email, 'password1')); + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}`). + reply(200, TestHelper.basicChannel); + + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id}); + + await store.dispatch(Actions.joinChannel(TestHelper.basicUser.id, TestHelper.basicTeam.id, TestHelper.basicChannel.id)); + + const {channels, myMembers} = store.getState().entities.channels; + assert.ok(channels[TestHelper.basicChannel.id]); + assert.ok(myMembers[TestHelper.basicChannel.id]); + + nock(Client4.getBaseRoute()). + delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`). + reply(400, {}); + + nock(Client4.getBaseRoute()). + delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`). + reply(200, OK_RESPONSE); + + // This action will retry after 1000ms + await store.dispatch(Actions.leaveChannel(TestHelper.basicChannel.id)); + + setTimeout(test2, 300); + } + + async function test2() { + // retry will have completed and should have left the channel successfully + const {channels, myMembers} = store.getState().entities.channels; + + assert.ok(channels[TestHelper.basicChannel.id]); + assert.ifError(myMembers[TestHelper.basicChannel.id]); + done(); + } + + test(); + }); + + it('leave private channel', async () => { + const newChannel = { + team_id: TestHelper.basicTeam.id, + name: 'redux-test-private', + display_name: 'Redux Test', + purpose: 'This is to test redux', + header: 'MM with Redux', + type: 'P', + }; + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, {...TestHelper.fakeChannelWithId(TestHelper.basicTeam.id), ...newChannel}); + + const {data: channel} = await store.dispatch(Actions.createChannel(newChannel, TestHelper.basicUser.id)); + let channels = store.getState().entities.channels.channels; + assert.ok(channels[channel.id]); + + nock(Client4.getBaseRoute()). + delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.leaveChannel(channel.id)); + channels = store.getState().entities.channels.channels; + const myMembers = store.getState().entities.channels.myMembers; + assert.ok(!channels[channel.id]); + assert.ok(!myMembers[channel.id]); + }); + + it('joinChannel', async () => { + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}`). + reply(200, TestHelper.basicChannel); + + nock(Client4.getBaseRoute()). + post(`/channels/${TestHelper.basicChannel.id}/members`). + reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id}); + + await store.dispatch(Actions.joinChannel(TestHelper.basicUser.id, TestHelper.basicTeam.id, TestHelper.basicChannel.id)); + + const {channels, myMembers} = store.getState().entities.channels; + assert.ok(channels[TestHelper.basicChannel.id]); + assert.ok(myMembers[TestHelper.basicChannel.id]); + }); + + it('joinChannelByName', async () => { + const secondClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await secondClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const secondChannel = await secondClient.createChannel( + TestHelper.fakeChannel(TestHelper.basicTeam.id)); + + nock(Client4.getTeamsRoute()). + get(`/${TestHelper.basicTeam.id}/channels/name/${secondChannel.name}?include_deleted=true`). + reply(200, secondChannel); + + nock(Client4.getBaseRoute()). + post(`/channels/${secondChannel.id}/members`). + reply(201, {channel_id: secondChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id}); + + await store.dispatch(Actions.joinChannel( + TestHelper.basicUser.id, + TestHelper.basicTeam.id, + null, + secondChannel.name, + )); + + const {channels, myMembers} = store.getState().entities.channels; + assert.ok(channels[secondChannel.id]); + assert.ok(myMembers[secondChannel.id]); + }); + + it('favoriteChannel', async () => { + nock(Client4.getBaseRoute()). + put(`/users/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.favoriteChannel(TestHelper.basicChannel.id)); + + const state = store.getState(); + const prefKey = getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, TestHelper.basicChannel.id); + const preference = state.entities.preferences.myPreferences[prefKey]; + assert.ok(preference); + assert.ok(preference.value === 'true'); + }); + + it('unfavoriteChannel', async () => { + nock(Client4.getBaseRoute()). + put(`/users/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.favoriteChannel(TestHelper.basicChannel.id)); + + let state = store.getState(); + let prefKey = getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, TestHelper.basicChannel.id); + let preference = state.entities.preferences.myPreferences[prefKey]; + assert.ok(preference); + assert.ok(preference.value === 'true'); + + nock(Client4.getBaseRoute()). + delete(`/users/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + store.dispatch(Actions.unfavoriteChannel(TestHelper.basicChannel.id)); + + state = store.getState(); + prefKey = getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, TestHelper.basicChannel.id); + preference = state.entities.preferences.myPreferences[prefKey]; + assert.ok(!preference); + }); + + it('autocompleteChannels', async () => { + const prefix = TestHelper.basicChannel.name.slice(0, 5); + + nock(Client4.getTeamRoute(TestHelper.basicChannel.team_id)). + get('/channels/autocomplete'). + query({name: prefix}). + reply(200, [TestHelper.basicChannel]); + + const result = await store.dispatch(Actions.autocompleteChannels( + TestHelper.basicChannel.team_id, + prefix, + )); + + assert.deepEqual(result, {data: [TestHelper.basicChannel]}); + }); + + it('autocompleteChannelsForSearch', async () => { + const prefix = TestHelper.basicChannel.name.slice(0, 5); + + nock(Client4.getTeamRoute(TestHelper.basicChannel.team_id)). + get('/channels/search_autocomplete'). + query({name: prefix}). + reply(200, [TestHelper.basicChannel]); + + const result = await store.dispatch(Actions.autocompleteChannelsForSearch( + TestHelper.basicChannel.team_id, + prefix, + )); + + assert.deepEqual(result, {data: [TestHelper.basicChannel]}); + }); + + it('updateChannelScheme', async () => { + TestHelper.mockLogin(); + await store.dispatch(login(TestHelper.basicChannelMember.email, 'password1')); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + await store.dispatch(Actions.createChannel(TestHelper.fakeChannel(TestHelper.basicTeam.id), TestHelper.basicUser.id)); + + const createRequest = store.getState().requests.channels.createChannel; + + if (createRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(createRequest.error)); + } + + const {channels} = store.getState().entities.channels; + const schemeId = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'; + const {id} = channels[Object.keys(channels)[0]]; + + nock(Client4.getBaseRoute()). + put('/channels/' + id + '/scheme'). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.updateChannelScheme(id, schemeId)); + const updated = store.getState().entities.channels.channels[id]; + assert.ok(updated); + assert.equal(updated.scheme_id, schemeId); + }); + + it('updateChannelMemberSchemeRoles', async () => { + TestHelper.mockLogin(); + await store.dispatch(login(TestHelper.basicChannelMember.email, 'password1')); + + nock(Client4.getBaseRoute()). + post('/channels'). + reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id)); + + const userId = TestHelper.basicChannelMember.id; + + await store.dispatch(Actions.createChannel(TestHelper.fakeChannel(TestHelper.basicTeam.id), userId)); + + const {channels} = store.getState().entities.channels; + const channelId = channels[Object.keys(channels)[0]].id; + + nock(Client4.getBaseRoute()). + put(`/channels/${channelId}/members/${userId}/schemeRoles`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.updateChannelMemberSchemeRoles(channelId, userId, true, true)); + const update1 = store.getState().entities.channels.membersInChannel[channelId][userId]; + assert.ok(update1); + assert.equal(update1.scheme_admin, true); + assert.equal(update1.scheme_user, true); + + nock(Client4.getBaseRoute()). + put(`/channels/${channelId}/members/${userId}/schemeRoles`). + reply(200, OK_RESPONSE); + + await store.dispatch(Actions.updateChannelMemberSchemeRoles(channelId, userId, false, false)); + + const update2 = store.getState().entities.channels.membersInChannel[channelId][userId]; + assert.ok(update2); + assert.equal(update2.scheme_admin, false); + assert.equal(update2.scheme_user, false); + }); + + it('markGroupChannelOpen', async () => { + const channelId = TestHelper.generateId(); + const now = new Date().getTime(); + + nock(Client4.getBaseRoute()). + put(`/users/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + + await Actions.markGroupChannelOpen(channelId)(store.dispatch, store.getState); + + const state = store.getState(); + let prefKey = getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId); + let preference = state.entities.preferences.myPreferences[prefKey]; + assert.ok(preference); + assert.ok(preference.value === 'true'); + + prefKey = getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channelId); + preference = state.entities.preferences.myPreferences[prefKey]; + assert.ok(preference); + assert.ok(parseInt(preference.value, 10) >= now); + }); + + it('getChannelTimezones', async () => { + const {dispatch, getState} = store; + const channelId = TestHelper.basicChannel.id; + const response = { + useAutomaticTimezone: 'true', + manualTimezone: '', + automaticTimezone: 'xoxoxo/blablabla', + }; + + nock(Client4.getBaseRoute()). + get(`/channels/${TestHelper.basicChannel.id}/timezones`). + query(true). + reply(200, response); + + const {data} = await Actions.getChannelTimezones(channelId)(dispatch, getState); + + assert.deepEqual(response, data); + }); + + it('membersMinusGroupMembers', async () => { + const channelID = 'cid10000000000000000000000'; + const groupIDs = ['gid10000000000000000000000', 'gid20000000000000000000000']; + const page = 7; + const perPage = 63; + + nock(Client4.getBaseRoute()).get( + `/channels/${channelID}/members_minus_group_members?group_ids=${groupIDs.join(',')}&page=${page}&per_page=${perPage}`). + reply(200, {users: [], total_count: 0}); + + const {error} = await Actions.membersMinusGroupMembers(channelID, groupIDs, page, perPage)(store.dispatch, store.getState); + + assert.equal(error, null); + }); + + it('getChannelModerations', async () => { + const channelID = 'cid10000000000000000000000'; + + nock(Client4.getBaseRoute()).get( + `/channels/${channelID}/moderations`). + reply(200, [{ + name: Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_POST, + roles: { + members: true, + guests: false, + }, + }]); + + const {error} = await store.dispatch(Actions.getChannelModerations(channelID)); + const moderations = store.getState().entities.channels.channelModerations[channelID]; + + assert.equal(error, null); + assert.equal(moderations[0].name, Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_POST); + assert.equal(moderations[0].roles.members, true); + assert.equal(moderations[0].roles.guests, false); + }); + + it('patchChannelModerations', async () => { + const channelID = 'cid10000000000000000000000'; + + nock(Client4.getBaseRoute()).put( + `/channels/${channelID}/moderations/patch`). + reply(200, [{ + name: Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_REACTIONS, + roles: { + members: true, + guests: false, + }, + }]); + + const {error} = await store.dispatch(Actions.patchChannelModerations(channelID, {})); + const moderations = store.getState().entities.channels.channelModerations[channelID]; + + assert.equal(error, null); + assert.equal(moderations[0].name, Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_REACTIONS); + assert.equal(moderations[0].roles.members, true); + assert.equal(moderations[0].roles.guests, false); + }); +}); diff --git a/app/mm-redux/actions/channels.ts b/app/mm-redux/actions/channels.ts new file mode 100644 index 000000000..e22bfd4b0 --- /dev/null +++ b/app/mm-redux/actions/channels.ts @@ -0,0 +1,1515 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {General, Preferences} from '../constants'; +import {ChannelTypes, PreferenceTypes, UserTypes} from '@mm-redux/action_types'; +import {savePreferences, deletePreferences} from './preferences'; +import {compareNotifyProps, getChannelsIdForTeam, getChannelByName} from '@mm-redux/utils/channel_utils'; +import { + getChannelsNameMapInTeam, + getMyChannelMember as getMyChannelMemberSelector, + getRedirectChannelNameForTeam, + isManuallyUnread, +} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getConfig, getServerVersion} from '@mm-redux/selectors/entities/general'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; + +import {Action, ActionFunc, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; + +import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch} from '@mm-redux/types/channels'; + +import {PreferenceType} from '@mm-redux/types/preferences'; + +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +import {getMissingProfilesByIds} from './users'; +import {loadRolesIfNeeded} from './roles'; + +export function selectChannel(channelId: string) { + return { + type: ChannelTypes.SELECT_CHANNEL, + data: channelId, + }; +} + +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, + }, + ])); + + 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(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: Array = [ + {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: Array): 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 = { + 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(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 updateChannelNotifyProps(userId: string, channelId: string, props: ChannelNotifyProps): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const notifyProps = { + user_id: userId, + channel_id: channelId, + ...props, + }; + + try { + await Client4.updateChannelNotifyProps(notifyProps); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + + return {error}; + } + + const member = getState().entities.channels.myMembers[channelId] || {}; + const currentNotifyProps = member.notify_props || {}; + + // This triggers a re-sorting of channel sidebar, so ensure it's called only when + // notification settings for a channel actually change. + if (!compareNotifyProps(notifyProps, currentNotifyProps)) { + dispatch({ + type: ChannelTypes.RECEIVED_CHANNEL_PROPS, + data: { + channel_id: channelId, + notifyProps: {...currentNotifyProps, ...notifyProps}, + }, + }); + } + + return {data: true}; + }; +} + +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 getChannelTimezones(channelId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let channelTimezones; + try { + const channelTimezonesRequest = Client4.getChannelTimezones(channelId); + + channelTimezones = await channelTimezonesRequest; + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + return {data: channelTimezones}; + }; +} + +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(); + 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(); + + 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 getChannelMembers(channelId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let channelMembers: ChannelMembership[]; + + try { + const channelMembersRequest = Client4.getChannelMembers(channelId, page, perPage); + + channelMembers = await channelMembersRequest; + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const userIds = channelMembers.map((cm) => cm.user_id); + getMissingProfilesByIds(userIds)(dispatch, getState); + + dispatch({ + type: ChannelTypes.RECEIVED_CHANNEL_MEMBERS, + data: channelMembers, + }); + + 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, + }, + meta: { + offline: { + effect: () => Client4.removeFromChannel(currentUserId, channelId), + commit: {type: 'do_nothing'}, // redux-offline always needs to dispatch something on commit + rollback: () => { + dispatch(batchActions([ + { + type: ChannelTypes.RECEIVED_CHANNEL, + data: channel, + }, + { + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + data: member, + }, + ])); + }, + }, + }, + }); + + return {data: true}; + }; +} + +export function joinChannel(userId: string, teamId: string, channelId: string, channelName: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let member: ChannelMembership | undefined | null; + let channel; + try { + if (channelId) { + member = await Client4.addToChannel(userId, channelId); + channel = await Client4.getChannel(channelId); + } else if (channelName) { + 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, + }, + ])); + 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 markChannelAsViewed(channelId: string, prevChannelId = ''): ActionFunc { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const actions: Action[] = []; + + const {myMembers} = getState().entities.channels; + const member = myMembers[channelId]; + const state = getState(); + if (member) { + actions.push({ + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + data: {...member, last_viewed_at: Date.now()}, + }); + if (isManuallyUnread(state, channelId)) { + actions.push({ + type: ChannelTypes.REMOVE_MANUALLY_UNREAD, + data: {channelId}, + }); + } + + dispatch(loadRolesIfNeeded(member.roles.split(' '))); + } + + const prevMember = myMembers[prevChannelId]; + if (prevMember && !isManuallyUnread(getState(), prevChannelId)) { + actions.push({ + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + data: {...prevMember, last_viewed_at: Date.now()}, + }); + dispatch(loadRolesIfNeeded(prevMember.roles.split(' '))); + } + + if (actions.length) { + 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): 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); + } 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, notAssociatedToGroup = '', excludeDefaultChannels = false, page?: number, perPage?: number): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null}); + + let response; + try { + response = await Client4.searchAllChannels(term, notAssociatedToGroup, excludeDefaultChannels, page, perPage); + } 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 searchGroupChannels(term: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.searchGroupChannels, + params: [term], + }); +} + +export function getChannelStats(channelId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let stat; + try { + stat = await Client4.getChannelStats(channelId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: ChannelTypes.RECEIVED_CHANNEL_STATS, + data: stat, + }); + + return {data: stat}; + }; +} + +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 updateChannelMemberRoles(channelId: string, userId: string, roles: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateChannelMemberRoles(channelId, userId, roles); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const membersInChannel = getState().entities.channels.membersInChannel[channelId]; + if (membersInChannel && membersInChannel[userId]) { + dispatch({ + type: ChannelTypes.RECEIVED_CHANNEL_MEMBER, + data: {...membersInChannel[userId], roles}, + }); + } + + return {data: true}; + }; +} + +export function updateChannelHeader(channelId: string, header: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + 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, getState: GetStateFunc) => { + 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, + }, + }); + + actions.push({ + type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT, + data: { + teamId: channel.team_id, + channelId, + amount: channelMember.mention_count, + }, + }); + } + + 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, + }, + }); + + actions.push({ + type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT, + data: { + teamId: prevChannel.team_id, + channelId: prevChannelId, + amount: prevChannelMember.mention_count, + }, + }); + } + + 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: Array): 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_TOTAL_MSG_COUNT, + data: { + channelId, + amount: 1, + }, + }, { + type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT, + data: { + teamId, + channelId, + amount: 1, + onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props && + myMembers[channelId].notify_props.mark_unread === General.MENTION, + }, + }]; + + if (mentions && mentions.indexOf(currentUserId) !== -1) { + actions.push({ + type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT, + data: { + teamId, + channelId, + amount: 1, + }, + }); + } + + dispatch(batchActions(actions)); + + return {data: true}; + }; +} + +export function getChannelMembersByIds(channelId: string, userIds: Array) { + return bindClientFunc({ + clientFunc: Client4.getChannelMembersByIds, + onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBERS, + params: [ + channelId, + userIds, + ], + }); +} + +export function getChannelMember(channelId: string, userId: string) { + return bindClientFunc({ + clientFunc: Client4.getChannelMember, + onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBER, + params: [ + channelId, + userId, + ], + }); +} + +export function getMyChannelMember(channelId: string) { + return bindClientFunc({ + clientFunc: Client4.getMyChannelMember, + onSuccess: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + params: [ + channelId, + ], + }); +} + +export function favoriteChannel(channelId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + const preference: PreferenceType = { + user_id: currentUserId, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, + name: channelId, + value: 'true', + }; + + Client4.trackEvent('action', 'action_channels_favorite'); + + return savePreferences(currentUserId, [preference])(dispatch); + }; +} + +export function unfavoriteChannel(channelId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + const preference: PreferenceType = { + user_id: currentUserId, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, + name: channelId, + value: '', + }; + + Client4.trackEvent('action', 'action_channels_unfavorite'); + + return deletePreferences(currentUserId, [preference])(dispatch, getState); + }; +} + +export function updateChannelScheme(channelId: string, schemeId: string) { + return bindClientFunc({ + clientFunc: async () => { + await Client4.updateChannelScheme(channelId, schemeId); + return {channelId, schemeId}; + }, + onSuccess: ChannelTypes.UPDATED_CHANNEL_SCHEME, + }); +} + +export function updateChannelMemberSchemeRoles(channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) { + return bindClientFunc({ + clientFunc: async () => { + await Client4.updateChannelMemberSchemeRoles(channelId, userId, isSchemeUser, isSchemeAdmin); + return {channelId, userId, isSchemeUser, isSchemeAdmin}; + }, + onSuccess: ChannelTypes.UPDATED_CHANNEL_MEMBER_SCHEME_ROLES, + }); +} + +export function membersMinusGroupMembers(channelID: string, groupIDs: Array, 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: Array): ActionFunc { + return bindClientFunc({ + clientFunc: async () => { + const moderations = await Client4.patchChannelModerations(channelId, patch); + return {channelId, moderations}; + }, + onSuccess: ChannelTypes.RECEIVED_CHANNEL_MODERATIONS, + params: [ + channelId, + ], + }); +} + +export default { + selectChannel, + createChannel, + createDirectChannel, + updateChannel, + patchChannel, + updateChannelNotifyProps, + getChannel, + fetchMyChannelsAndMembers, + getMyChannelMembers, + getChannelTimezones, + getChannelMembersByIds, + leaveChannel, + joinChannel, + deleteChannel, + unarchiveChannel, + viewChannel, + markChannelAsViewed, + getChannels, + autocompleteChannels, + autocompleteChannelsForSearch, + searchChannels, + searchGroupChannels, + getChannelStats, + addChannelMember, + removeChannelMember, + updateChannelHeader, + updateChannelPurpose, + markChannelAsRead, + markChannelAsUnread, + favoriteChannel, + unfavoriteChannel, + membersMinusGroupMembers, + getChannelModerations, +}; diff --git a/app/mm-redux/actions/emojis.test.js b/app/mm-redux/actions/emojis.test.js new file mode 100644 index 000000000..46022081c --- /dev/null +++ b/app/mm-redux/actions/emojis.test.js @@ -0,0 +1,411 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'fs'; +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/emojis'; +import {Client4} from '@mm-redux/client'; + +import {GeneralTypes} from '@mm-redux/action_types'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Emojis', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('createCustomEmoji', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + const state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created.id]); + }); + + it('getCustomEmojis', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/emoji'). + query(true). + reply(200, [created]); + + await Actions.getCustomEmojis()(store.dispatch, store.getState); + + const state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created.id]); + }); + + it('getAllCustomEmojis', async () => { + store.dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: '4.0.0'}); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + const {data: created1} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + fs.createReadStream('test/assets/images/test.png'), + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + const {data: created2} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + fs.createReadStream('test/assets/images/test.png'), + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/emoji'). + query(true). + reply(200, [created1]); + + nock(Client4.getBaseRoute()). + get('/emoji'). + query(true). + reply(200, [created2]); + + nock(Client4.getBaseRoute()). + get('/emoji'). + query(true). + reply(200, []); + await Actions.getAllCustomEmojis(1)(store.dispatch, store.getState); + + let state = store.getState(); + + let emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created1.id]); + assert.ok(emojis[created2.id]); + + nock(Client4.getBaseRoute()). + delete(`/emoji/${created2.id}`). + reply(200, OK_RESPONSE); + + // Should have all emojis minus the deleted one + await Client4.deleteCustomEmoji(created2.id); + + nock(Client4.getBaseRoute()). + get('/emoji'). + query(true). + reply(200, [created1]); + + nock(Client4.getBaseRoute()). + get('/emoji'). + query(true). + reply(200, []); + await Actions.getAllCustomEmojis(1)(store.dispatch, store.getState); + + state = store.getState(); + + emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created1.id]); + assert.ok(!emojis[created2.id]); + + nock(Client4.getBaseRoute()). + delete(`/emoji/${created1.id}`). + reply(200, OK_RESPONSE); + + // Cleanup + Client4.deleteCustomEmoji(created1.id); + }); + + it('deleteCustomEmoji', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + delete(`/emoji/${created.id}`). + reply(200, OK_RESPONSE); + + await Actions.deleteCustomEmoji(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(!emojis[created.id]); + }); + + it('loadProfilesForCustomEmojis', async () => { + const fakeUser = TestHelper.fakeUser(); + fakeUser.id = TestHelper.generateId(); + const junkUserId = TestHelper.generateId(); + + const testEmojis = [{ + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + { + name: TestHelper.generateId(), + creator_id: fakeUser.id, + }, + { + name: TestHelper.generateId(), + creator_id: junkUserId, + }]; + + nock(Client4.getUsersRoute()). + post('/ids'). + reply(200, [TestHelper.basicUser, fakeUser]); + + await store.dispatch(Actions.loadProfilesForCustomEmojis(testEmojis)); + + const state = store.getState(); + const profiles = state.entities.users.profiles; + assert.ok(profiles[TestHelper.basicUser.id]); + assert.ok(profiles[fakeUser.id]); + assert.ok(!profiles[junkUserId]); + }); + + it('searchCustomEmojis', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post('/emoji/search'). + reply(200, [created]); + + await Actions.searchCustomEmojis(created.name, {prefix_only: true})(store.dispatch, store.getState); + + const state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created.id]); + }); + + it('autocompleteCustomEmojis', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/emoji/autocomplete'). + query(true). + reply(200, [created]); + + await Actions.autocompleteCustomEmojis(created.name)(store.dispatch, store.getState); + + const state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created.id]); + }); + + it('getCustomEmoji', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/emoji/${created.id}`). + reply(200, created); + + await Actions.getCustomEmoji(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created.id]); + }); + + it('getCustomEmojiByName', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/emoji/name/${created.name}`). + reply(200, created); + + await Actions.getCustomEmojiByName(created.name)(store.dispatch, store.getState); + + let state = store.getState(); + + const emojis = state.entities.emojis.customEmoji; + assert.ok(emojis); + assert.ok(emojis[created.id]); + + const missingName = TestHelper.generateId(); + + nock(Client4.getBaseRoute()). + get(`/emoji/name/${missingName}`). + reply(404, {message: 'Not found', status_code: 404}); + + await Actions.getCustomEmojiByName(missingName)(store.dispatch, store.getState); + + state = store.getState(); + assert.ok(state.entities.emojis.nonExistentEmoji.has(missingName)); + }); + + it('getCustomEmojisByName', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/emoji/name/${created.name}`). + reply(200, created); + + const missingName = TestHelper.generateId(); + + nock(Client4.getBaseRoute()). + get(`/emoji/name/${missingName}`). + reply(404, {message: 'Not found', status_code: 404}); + + await Actions.getCustomEmojisByName([created.name, missingName])(store.dispatch, store.getState); + + const state = store.getState(); + assert.ok(state.entities.emojis.customEmoji[created.id]); + assert.ok(state.entities.emojis.nonExistentEmoji.has(missingName)); + }); + + it('getCustomEmojisInText', async () => { + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getBaseRoute()). + post('/emoji'). + reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + const {data: created} = await Actions.createCustomEmoji( + { + name: TestHelper.generateId(), + creator_id: TestHelper.basicUser.id, + }, + testImageData, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/emoji/name/${created.name}`). + reply(200, created); + + const missingName = TestHelper.generateId(); + + nock(Client4.getBaseRoute()). + get(`/emoji/name/${missingName}`). + reply(404, {message: 'Not found', status_code: 404}); + + await Actions.getCustomEmojisInText(`some text :${created.name}: :${missingName}:`)(store.dispatch, store.getState); + + const state = store.getState(); + assert.ok(state.entities.emojis.customEmoji[created.id]); + assert.ok(state.entities.emojis.nonExistentEmoji.has(missingName)); + }); +}); diff --git a/app/mm-redux/actions/emojis.ts b/app/mm-redux/actions/emojis.ts new file mode 100644 index 000000000..779f91039 --- /dev/null +++ b/app/mm-redux/actions/emojis.ts @@ -0,0 +1,250 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {EmojiTypes} from '@mm-redux/action_types'; +import {General, Emoji} from '../constants'; +import {getProfilesByIds} from './users'; +import {getCustomEmojisByName as selectCustomEmojisByName} from '@mm-redux/selectors/entities/emojis'; +import {parseNeededCustomEmojisFromText} from '@mm-redux/utils/emoji_utils'; + +import {GetStateFunc, DispatchFunc, ActionFunc, ActionResult} from '@mm-redux/types/actions'; + +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +import {SystemEmoji, CustomEmoji} from '@mm-redux/types/emojis'; +import {Dictionary} from '@mm-redux/types/utilities'; +export let systemEmojis: Map = new Map(); +export function setSystemEmojis(emojis: Map) { + systemEmojis = emojis; +} + +export function createCustomEmoji(emoji: any, image: any): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createCustomEmoji, + onSuccess: EmojiTypes.RECEIVED_CUSTOM_EMOJI, + params: [ + emoji, + image, + ], + }); +} + +export function getCustomEmoji(emojiId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getCustomEmoji, + onSuccess: EmojiTypes.RECEIVED_CUSTOM_EMOJI, + params: [ + emojiId, + ], + }); +} + +export function getCustomEmojiByName(name: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + + try { + data = await Client4.getCustomEmojiByName(name); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + if (error.status_code === 404) { + dispatch({type: EmojiTypes.CUSTOM_EMOJI_DOES_NOT_EXIST, data: name}); + } else { + dispatch(logError(error)); + } + + return {error}; + } + + dispatch({ + type: EmojiTypes.RECEIVED_CUSTOM_EMOJI, + data, + }); + + return {data}; + }; +} + +export function getCustomEmojisByName(names: Array): ActionFunc { + return async (dispatch: DispatchFunc) => { + if (!names || names.length === 0) { + return {data: true}; + } + + const promises: Promise[] = []; + names.forEach((name) => promises.push(dispatch(getCustomEmojiByName(name)))); + + await Promise.all(promises); + return {data: true}; + }; +} + +export function getCustomEmojisInText(text: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (!text) { + return {data: true}; + } + + const state = getState(); + const nonExistentEmoji = state.entities.emojis.nonExistentEmoji; + const customEmojisByName = selectCustomEmojisByName(state); + + const emojisToLoad = parseNeededCustomEmojisFromText(text, systemEmojis, customEmojisByName, nonExistentEmoji); + + return getCustomEmojisByName(Array.from(emojisToLoad))(dispatch, getState); + }; +} + +export function getCustomEmojis( + page = 0, + perPage: number = General.PAGE_SIZE_DEFAULT, + sort: string = Emoji.SORT_BY_NAME, + loadUsers = false +): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getCustomEmojis(page, perPage, sort); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + if (loadUsers) { + dispatch(loadProfilesForCustomEmojis(data)); + } + + dispatch({ + type: EmojiTypes.RECEIVED_CUSTOM_EMOJIS, + data, + }); + + return {data}; + }; +} + +export function loadProfilesForCustomEmojis(emojis: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const usersToLoad: Dictionary = {}; + emojis.forEach((emoji: CustomEmoji) => { + if (!getState().entities.users.profiles[emoji.creator_id]) { + usersToLoad[emoji.creator_id] = true; + } + }); + + const userIds = Object.keys(usersToLoad); + + if (userIds.length > 0) { + await dispatch(getProfilesByIds(userIds)); + } + + return {data: true}; + }; +} + +export function getAllCustomEmojis(perPage: number = General.PAGE_SIZE_MAXIMUM): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: EmojiTypes.CLEAR_CUSTOM_EMOJIS, + data: null, + }); + + let hasMore = true; + let page = 0; + const allEmojis = []; + + do { + try { + let emojis = []; + emojis = await Client4.getCustomEmojis(page, perPage, Emoji.SORT_BY_NAME); // eslint-disable-line no-await-in-loop + if (emojis.length < perPage) { + hasMore = false; + } else { + page += 1; + } + allEmojis.push(...emojis); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error: true}; + } + } while (hasMore); + + dispatch({ + type: EmojiTypes.RECEIVED_CUSTOM_EMOJIS, + data: allEmojis, + }); + + return {data: true}; + }; +} + +export function deleteCustomEmoji(emojiId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.deleteCustomEmoji(emojiId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: EmojiTypes.DELETED_CUSTOM_EMOJI, + data: {id: emojiId}, + }); + + return {data: true}; + }; +} + +export function searchCustomEmojis(term: string, options: any = {}, loadUsers = false): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.searchCustomEmoji(term, options); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + if (loadUsers) { + dispatch(loadProfilesForCustomEmojis(data)); + } + + dispatch({ + type: EmojiTypes.RECEIVED_CUSTOM_EMOJIS, + data, + }); + + return {data}; + }; +} + +export function autocompleteCustomEmojis(name: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.autocompleteCustomEmoji(name); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: EmojiTypes.RECEIVED_CUSTOM_EMOJIS, + data, + }); + + return {data}; + }; +} diff --git a/app/mm-redux/actions/errors.test.js b/app/mm-redux/actions/errors.test.js new file mode 100644 index 000000000..95283e0a7 --- /dev/null +++ b/app/mm-redux/actions/errors.test.js @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import {logError} from '@mm-redux/actions/errors'; +import {Client4} from '@mm-redux/client'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.Errors', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + Client4.setEnableLogging(true); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + Client4.setEnableLogging(false); + }); + + it('logError should hit /logs endpoint, unless server error', async () => { + let count = 0; + + nock(Client4.getBaseRoute()). + post('/logs'). + reply(200, () => { + count++; + return '{}'; + }). + post('/logs'). + reply(200, () => { + count++; + return '{}'; + }). + post('/logs'). + reply(200, () => { + count++; + return '{}'; + }); + + await store.dispatch(logError({message: 'error'})); + await store.dispatch(logError({message: 'error', server_error_id: 'error_id'})); + await store.dispatch(logError({message: 'error'})); + + if (count > 2) { + assert.fail(`should not hit /logs endpoint, called ${count} times`); + } + + await store.dispatch(logError({message: 'error', server_error_id: 'api.context.session_expired.app_error'})); + + if (count > 2) { + assert.fail('should not add session expired errors to the reducer'); + } + }); +}); diff --git a/app/mm-redux/actions/errors.ts b/app/mm-redux/actions/errors.ts new file mode 100644 index 000000000..69e566309 --- /dev/null +++ b/app/mm-redux/actions/errors.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {ErrorTypes} from '@mm-redux/action_types'; +import {serializeError, ErrorObject} from 'serialize-error'; +import {Client4} from '@mm-redux/client'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {DispatchFunc, ActionFunc} from '@mm-redux/types/actions'; +import {Error} from '@mm-redux/types/errors'; +export function dismissErrorObject(index: number) { + return { + type: ErrorTypes.DISMISS_ERROR, + index, + data: null, + }; +} + +export function dismissError(index: number): ActionFunc { + return async (dispatch: DispatchFunc) => { + dispatch(dismissErrorObject(index)); + + return {data: true}; + }; +} + +export function getLogErrorAction(error: ErrorObject, displayable = false) { + return { + type: ErrorTypes.LOG_ERROR, + displayable, + error, + data: null, + }; +} + +export function logError(error: Error, displayable = false): ActionFunc { + return async (dispatch: DispatchFunc) => { + if (error.server_error_id === 'api.context.session_expired.app_error') { + return {data: true}; + } + + const serializedError = serializeError(error); + + let sendToServer = true; + if (error.stack && error.stack.includes('TypeError: Failed to fetch')) { + sendToServer = false; + } + if (error.server_error_id) { + sendToServer = false; + } + + if (sendToServer) { + try { + const stringifiedSerializedError = JSON.stringify(serializedError).toString(); + await Client4.logClientError(stringifiedSerializedError); + } catch (err) { + // avoid crashing the app if an error sending + // the error occurs. + } + } + + EventEmitter.emit(ErrorTypes.LOG_ERROR, error); + dispatch(getLogErrorAction(serializedError, displayable)); + + return {data: true}; + }; +} + +export function clearErrors(): ActionFunc { + return async (dispatch: DispatchFunc) => { + dispatch({type: ErrorTypes.CLEAR_ERRORS, data: null}); + + return {data: true}; + }; +} diff --git a/app/mm-redux/actions/files.test.js b/app/mm-redux/actions/files.test.js new file mode 100644 index 000000000..9f939071c --- /dev/null +++ b/app/mm-redux/actions/files.test.js @@ -0,0 +1,94 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'fs'; +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/files'; +import {Client4} from '@mm-redux/client'; +import {RequestStatus} from '../constants'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const FormData = require('form-data'); + +describe('Actions.Files', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('getFilesForPost', async () => { + const {basicClient4, basicChannel} = TestHelper; + const testFileName = 'test.png'; + const testImageData = fs.createReadStream(`test/assets/images/${testFileName}`); + const clientId = TestHelper.generateId(); + + const imageFormData = new FormData(); + imageFormData.append('files', testImageData); + imageFormData.append('channel_id', basicChannel.id); + imageFormData.append('client_ids', clientId); + const formBoundary = imageFormData.getBoundary(); + + nock(Client4.getBaseRoute()). + post('/files'). + reply(201, {file_infos: [{id: TestHelper.generateId(), user_id: TestHelper.basicUser.id, create_at: 1507921547541, update_at: 1507921547541, delete_at: 0, name: 'test.png', extension: 'png', size: 258428, mime_type: 'image/png', width: 600, height: 600, has_preview_image: true}], client_ids: [TestHelper.generateId()]}); + + const fileUploadResp = await basicClient4. + uploadFile(imageFormData, formBoundary); + const fileId = fileUploadResp.file_infos[0].id; + + const fakePostForFile = TestHelper.fakePost(basicChannel.id); + fakePostForFile.file_ids = [fileId]; + + nock(Client4.getBaseRoute()). + post('/posts'). + reply(201, {...TestHelper.fakePostWithId(), ...fakePostForFile}); + const postForFile = await basicClient4.createPost(fakePostForFile); + + nock(Client4.getBaseRoute()). + get(`/posts/${postForFile.id}/files/info`). + reply(200, [{id: fileId, user_id: TestHelper.basicUser.id, create_at: 1507921547541, update_at: 1507921547541, delete_at: 0, name: 'test.png', extension: 'png', size: 258428, mime_type: 'image/png', width: 600, height: 600, has_preview_image: true}]); + + await Actions.getFilesForPost(postForFile.id)(store.dispatch, store.getState); + + const {files: allFiles, fileIdsByPostId} = store.getState().entities.files; + + assert.ok(allFiles); + assert.ok(allFiles[fileId]); + assert.equal(allFiles[fileId].id, fileId); + assert.equal(allFiles[fileId].name, testFileName); + + assert.ok(fileIdsByPostId); + assert.ok(fileIdsByPostId[postForFile.id]); + assert.equal(fileIdsByPostId[postForFile.id][0], fileId); + }); + + it('getFilePublicLink', async () => { + const fileId = 't1izsr9uspgi3ynggqu6xxjn9y'; + nock(Client4.getBaseRoute()). + get(`/files/${fileId}/link`). + query(true). + reply(200, { + link: 'https://mattermost.com/files/ndans23ry2rtjd1z73g6i5f3fc/public?h=rE1-b2N1VVVMsAQssjwlfNawbVOwUy1TRDuTeGC_tys', + }); + + await Actions.getFilePublicLink(fileId)(store.dispatch, store.getState); + + const state = store.getState(); + + const filePublicLink = state.entities.files.filePublicLink.link; + assert.equal('https://mattermost.com/files/ndans23ry2rtjd1z73g6i5f3fc/public?h=rE1-b2N1VVVMsAQssjwlfNawbVOwUy1TRDuTeGC_tys', filePublicLink); + assert.ok(filePublicLink); + assert.ok(filePublicLink.length > 0); + }); +}); diff --git a/app/mm-redux/actions/files.ts b/app/mm-redux/actions/files.ts new file mode 100644 index 000000000..9ee996d70 --- /dev/null +++ b/app/mm-redux/actions/files.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {FileTypes} from '@mm-redux/action_types'; + +import {Action, batchActions, DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions'; + +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +import {FileUploadResponse} from '@mm-redux/types/files'; + +export function getFilesForPost(postId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let files; + + try { + files = await Client4.getFileInfosForPost(postId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: FileTypes.RECEIVED_FILES_FOR_POST, + data: files, + postId, + }); + + return {data: true}; + }; +} + +export function getFilePublicLink(fileId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getFilePublicLink, + onSuccess: FileTypes.RECEIVED_FILE_PUBLIC_LINK, + params: [ + fileId, + ], + }); +} diff --git a/app/mm-redux/actions/general.test.js b/app/mm-redux/actions/general.test.js new file mode 100644 index 000000000..8aab12cb2 --- /dev/null +++ b/app/mm-redux/actions/general.test.js @@ -0,0 +1,172 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import {FormattedError} from './helpers.ts'; +import {GeneralTypes} from '@mm-redux/action_types'; +import * as Actions from '@mm-redux/actions/general'; +import {Client4} from '@mm-redux/client'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.General', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('getPing - Invalid URL', async () => { + const serverUrl = Client4.getUrl(); + Client4.setUrl('notarealurl'); + + const pingError = new FormattedError( + 'mobile.server_ping_failed', + 'Cannot connect to the server. Please check your server URL and internet connection.', + ); + + nock(Client4.getBaseRoute()). + get('/system/ping'). + query(true). + reply(401, {error: 'ping error', code: 401}); + + const {error} = await Actions.getPing()(store.dispatch, store.getState); + Client4.setUrl(serverUrl); + assert.deepEqual(error, pingError); + }); + + it('getPing', async () => { + const response = { + status: 'OK', + version: '4.0.0', + }; + + nock(Client4.getBaseRoute()). + get('/system/ping'). + query(true). + reply(200, response); + + const {data} = await Actions.getPing()(store.dispatch, store.getState); + assert.deepEqual(data, response); + }); + + it('getClientConfig', async () => { + nock(Client4.getBaseRoute()). + get('/config/client'). + query(true). + reply(200, {Version: '4.0.0', BuildNumber: '3', BuildDate: 'Yesterday', BuildHash: '1234'}); + + await Actions.getClientConfig()(store.dispatch, store.getState); + + const clientConfig = store.getState().entities.general.config; + + // Check a few basic fields since they may change over time + assert.ok(clientConfig.Version); + assert.ok(clientConfig.BuildNumber); + assert.ok(clientConfig.BuildDate); + assert.ok(clientConfig.BuildHash); + }); + + it('getLicenseConfig', async () => { + nock(Client4.getBaseRoute()). + get('/license/client'). + query(true). + reply(200, {IsLicensed: 'false'}); + + await Actions.getLicenseConfig()(store.dispatch, store.getState); + + const licenseConfig = store.getState().entities.general.license; + + // Check a few basic fields since they may change over time + assert.notStrictEqual(licenseConfig.IsLicensed, undefined); + }); + + it('setServerVersion', async () => { + const version = '3.7.0'; + await Actions.setServerVersion(version)(store.dispatch, store.getState); + await TestHelper.wait(100); + const {serverVersion} = store.getState().entities.general; + assert.deepEqual(serverVersion, version); + }); + + it('getDataRetentionPolicy', async () => { + const responseData = { + message_deletion_enabled: true, + file_deletion_enabled: false, + message_retention_cutoff: Date.now(), + file_retention_cutoff: 0, + }; + + nock(Client4.getBaseRoute()). + get('/data_retention/policy'). + query(true). + reply(200, responseData); + + await Actions.getDataRetentionPolicy()(store.dispatch, store.getState); + await TestHelper.wait(100); + const {dataRetentionPolicy} = store.getState().entities.general; + assert.deepEqual(dataRetentionPolicy, responseData); + }); + + it('getTimezones', async () => { + nock(Client4.getBaseRoute()). + get('/system/timezones'). + query(true). + reply(200, ['America/New_York', 'America/Los_Angeles']); + + await Actions.getSupportedTimezones()(store.dispatch, store.getState); + + await TestHelper.wait(100); + const {timezones} = store.getState().entities.general; + assert.equal(timezones.length > 0, true); + assert.equal(timezones.length === 0, false); + }); + + describe('getRedirectLocation', () => { + it('old server', async () => { + store.dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: '5.0.0'}); + + const mock = nock(Client4.getBaseRoute()). + get('/redirect_location'). + reply(404); + + // Should return the original link + const result = await store.dispatch(Actions.getRedirectLocation('http://examp.le')); + assert.deepEqual(result.data, {location: 'http://examp.le'}); + + // Should not call the API on an old server + assert.equal(mock.isDone(), false); + }); + + it('should save the correct location', async () => { + store.dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: '5.3.0'}); + + nock(Client4.getBaseRoute()). + get('/redirect_location'). + query({url: 'http://examp.le'}). + reply(200, '{"location": "https://example.com"}'); + + // Save the found URL if it finds one + await store.dispatch(Actions.getRedirectLocation('http://examp.le')); + + const existingURL = store.getState().entities.posts.expandedURLs['http://examp.le']; + assert.equal(existingURL, 'https://example.com'); + + // Save the found URL if it finds one + await store.dispatch(Actions.getRedirectLocation('http://nonexisting.url')); + + const nonexistingURL = store.getState().entities.posts.expandedURLs['http://nonexisting.url']; + assert.equal(nonexistingURL, 'http://nonexisting.url'); + }); + }); +}); diff --git a/app/mm-redux/actions/general.ts b/app/mm-redux/actions/general.ts new file mode 100644 index 000000000..2832b1200 --- /dev/null +++ b/app/mm-redux/actions/general.ts @@ -0,0 +1,201 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; + +import {GeneralTypes} from '@mm-redux/action_types'; + +import {getServerVersion} from '@mm-redux/selectors/entities/general'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {GeneralState} from '@mm-redux/types/general'; +import {logLevel} from '@mm-redux/types/client4'; +import {GetStateFunc, DispatchFunc, ActionFunc, batchActions} from '@mm-redux/types/actions'; + +import {logError} from './errors'; +import {loadRolesIfNeeded} from './roles'; +import {loadMe} from './users'; +import {bindClientFunc, forceLogoutIfNecessary, FormattedError} from './helpers'; + +export function getPing(): ActionFunc { + return async () => { + let data; + let pingError = new FormattedError( + 'mobile.server_ping_failed', + 'Cannot connect to the server. Please check your server URL and internet connection.' + ); + try { + data = await Client4.ping(); + if (data.status !== 'OK') { + // successful ping but not the right return {data} + return {error: pingError}; + } + } catch (error) { // Client4Error + if (error.status_code === 401) { + // When the server requires a client certificate to connect. + pingError = error; + } + return {error: pingError}; + } + + return {data}; + }; +} + +export function resetPing(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: GeneralTypes.PING_RESET, data: {}}); + + return {data: true}; + }; +} + +export function getClientConfig(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getClientConfigOld(); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + return {error}; + } + + Client4.setEnableLogging(data.EnableDeveloper === 'true'); + Client4.setDiagnosticId(data.DiagnosticId); + + dispatch(batchActions([ + {type: GeneralTypes.CLIENT_CONFIG_RECEIVED, data}, + ])); + + return {data}; + }; +} + +export function getDataRetentionPolicy(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getDataRetentionPolicy(); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + { + type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, + error, + }, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + {type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data}, + ])); + + return {data}; + }; +} + +export function getLicenseConfig(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getClientLicenseOld, + onSuccess: [GeneralTypes.CLIENT_LICENSE_RECEIVED], + }); +} + +export function logClientError(message: string, level: logLevel = 'ERROR') { + return bindClientFunc({ + clientFunc: Client4.logClientError, + onRequest: GeneralTypes.LOG_CLIENT_ERROR_REQUEST, + onSuccess: GeneralTypes.LOG_CLIENT_ERROR_SUCCESS, + onFailure: GeneralTypes.LOG_CLIENT_ERROR_FAILURE, + params: [ + message, + level, + ], + }); +} + +export function setAppState(state: GeneralState['appState']): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: GeneralTypes.RECEIVED_APP_STATE, data: state}); + + return {data: true}; + }; +} + +export function setDeviceToken(token: GeneralState['deviceToken']): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN, data: token}); + + return {data: true}; + }; +} + +export function setServerVersion(serverVersion: string): ActionFunc { + return async (dispatch, getState: GetStateFunc) => { + dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: serverVersion}); + dispatch(loadRolesIfNeeded([])); + + return {data: true}; + }; +} + +export function setStoreFromLocalData(data: { token: string; url: string }): ActionFunc { + return async (dispatch: DispatchFunc, getState) => { + Client4.setToken(data.token); + Client4.setUrl(data.url); + + return loadMe()(dispatch, getState); + }; +} + +export function getSupportedTimezones() { + return bindClientFunc({ + clientFunc: Client4.getTimezones, + onRequest: GeneralTypes.SUPPORTED_TIMEZONES_REQUEST, + onSuccess: [GeneralTypes.SUPPORTED_TIMEZONES_RECEIVED, GeneralTypes.SUPPORTED_TIMEZONES_SUCCESS], + onFailure: GeneralTypes.SUPPORTED_TIMEZONES_FAILURE, + }); +} + +export function setUrl(url: string) { + Client4.setUrl(url); + return true; +} + +export function getRedirectLocation(url: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let pendingData: Promise; + if (isMinimumServerVersion(getServerVersion(getState()), 5, 3)) { + pendingData = Client4.getRedirectLocation(url); + } else { + pendingData = Promise.resolve({location: url}); + } + + let data; + try { + data = await pendingData; + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch({type: GeneralTypes.REDIRECT_LOCATION_FAILURE, data: {error, url}}); + return {error}; + } + + dispatch({type: GeneralTypes.REDIRECT_LOCATION_SUCCESS, data: {...data, url}}); + return {data}; + }; +} + +export default { + getPing, + getClientConfig, + getDataRetentionPolicy, + getSupportedTimezones, + getLicenseConfig, + logClientError, + setAppState, + setDeviceToken, + setServerVersion, + setStoreFromLocalData, + setUrl, + getRedirectLocation, +}; diff --git a/app/mm-redux/actions/gifs.ts b/app/mm-redux/actions/gifs.ts new file mode 100644 index 000000000..e1dc7b9d8 --- /dev/null +++ b/app/mm-redux/actions/gifs.ts @@ -0,0 +1,422 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {GifTypes} from '@mm-redux/action_types'; +import {Client4} from '@mm-redux/client'; +import gfycatSdk from '@mm-redux/utils/gfycat_sdk'; +import {DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; +import {GlobalState} from '@mm-redux/types/store'; + +// APP PROPS + +export function saveAppPropsRequest(props: any) { + return { + type: GifTypes.SAVE_APP_PROPS, + props, + }; +} + +export function saveAppProps(appProps: any) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {GfycatApiKey, GfycatApiSecret} = getState().entities.general.config; + gfycatSdk(GfycatApiKey!, GfycatApiSecret!).authenticate(); + dispatch(saveAppPropsRequest(appProps)); + }; +} + +// SEARCH + +export function selectSearchText(searchText: string) { + return { + type: GifTypes.SELECT_SEARCH_TEXT, + searchText, + }; +} + +export function updateSearchText(searchText: string) { + return { + type: GifTypes.UPDATE_SEARCH_TEXT, + searchText, + }; +} + +export function searchBarTextSave(searchBarText: string) { + return { + type: GifTypes.SAVE_SEARCH_BAR_TEXT, + searchBarText, + }; +} + +export function invalidateSearchText(searchText: string) { + return { + type: GifTypes.INVALIDATE_SEARCH_TEXT, + searchText, + }; +} + +export function requestSearch(searchText: string) { + return { + type: GifTypes.REQUEST_SEARCH, + searchText, + }; +} + +export function receiveSearch({searchText, count, start, json}: {searchText: string; count: number; start: number; json: any}) { + return { + type: GifTypes.RECEIVE_SEARCH, + searchText, + ...json, + count, + start, + currentPage: start / count, + receivedAt: Date.now(), + }; +} + +export function receiveSearchEnd(searchText: string) { + return { + type: GifTypes.RECEIVE_SEARCH_END, + searchText, + }; +} + +export function errorSearching(err: any, searchText: string) { + return { + type: GifTypes.SEARCH_FAILURE, + searchText, + err, + }; +} + +export function receiveCategorySearch({tagName, json}: {tagName: string; json: any}) { + return { + type: GifTypes.RECEIVE_CATEGORY_SEARCH, + searchText: tagName, + ...json, + receiveAt: Date.now(), + }; +} + +export function clearSearchResults() { + return { + type: GifTypes.CLEAR_SEARCH_RESULTS, + }; +} + +export function requestSearchById(gfyId: string) { + return { + type: GifTypes.SEARCH_BY_ID_REQUEST, + payload: { + gfyId, + }, + }; +} + +export function receiveSearchById(gfyId: string, gfyItem: any) { + return { + type: GifTypes.SEARCH_BY_ID_SUCCESS, + payload: { + gfyId, + gfyItem, + }, + }; +} + +export function errorSearchById(err: any, gfyId: string) { + return { + type: GifTypes.SEARCH_BY_ID_FAILURE, + err, + gfyId, + }; +} + +export function searchScrollPosition(scrollPosition: number) { + return { + type: GifTypes.SAVE_SEARCH_SCROLL_POSITION, + scrollPosition, + }; +} + +export function searchPriorLocation(priorLocation: number) { + return { + type: GifTypes.SAVE_SEARCH_PRIOR_LOCATION, + priorLocation, + }; +} + +export function searchGfycat({searchText, count = 30, startIndex = 0}: { searchText: string; count?: number; startIndex?: number}) { + let start = startIndex; + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {GfycatApiKey, GfycatApiSecret} = getState().entities.general.config; + const {resultsByTerm} = getState().entities.gifs.search; + if (resultsByTerm[searchText]) { + start = resultsByTerm[searchText].start + count; + } + dispatch(requestSearch(searchText)); + const sdk = gfycatSdk(GfycatApiKey!, GfycatApiSecret!); + sdk.authenticate(); + return sdk.search({search_text: searchText, count, start}).then((json: any) => { + if (json.errorMessage) { + // There was no results before + if (resultsByTerm[searchText].items) { + dispatch(receiveSearchEnd(searchText)); + } else { + dispatch(errorSearching(json, searchText)); + } + } else { + dispatch(updateSearchText(searchText)); + dispatch(cacheGifsRequest(json.gfycats)); + dispatch(receiveSearch({searchText, count, start, json})); + + const context = getState().entities.gifs.categories.tagsDict[searchText] ? + 'category' : + 'search'; + Client4.trackEvent( + 'gfycat', + 'views', + {context, count: json.gfycats.length, keyword: searchText} + ); + } + }).catch( + (err: any) => dispatch(errorSearching(err, searchText)) + ); + }; +} + +export function searchCategory({tagName = '', gfyCount = 30, cursorPos = undefined}) { + let cursor = cursorPos; + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {GfycatApiKey, GfycatApiSecret} = getState().entities.general.config; + const {resultsByTerm} = getState().entities.gifs.search; + if (resultsByTerm[tagName]) { + cursor = resultsByTerm[tagName].cursor; + } + dispatch(requestSearch(tagName)); + return gfycatSdk(GfycatApiKey!, GfycatApiSecret!).getTrendingCategories({tagName, gfyCount, cursor}).then( + (json: any) => { + if (json.errorMessage) { + if (resultsByTerm[tagName].gfycats) { + dispatch(receiveSearchEnd(tagName)); + } else { + dispatch(errorSearching(json, tagName)); + } + } else { + dispatch(updateSearchText(tagName)); + dispatch(cacheGifsRequest(json.gfycats)); + dispatch(receiveCategorySearch({tagName, json})); + + Client4.trackEvent( + 'gfycat', + 'views', + {context: 'category', count: json.gfycats.length, keyword: tagName} + ); + + // preload categories list + if (tagName === 'trending') { + dispatch(requestCategoriesListIfNeeded() as any); + } + } + } + ).catch((err: any) => dispatch(errorSearching(err, tagName))); + }; +} + +export function shouldSearch(state: GlobalState, searchText: string) { + const resultsByTerm = state.entities.gifs.search.resultsByTerm[searchText]; + if (!resultsByTerm) { + return true; + } else if (resultsByTerm.isFetching) { + return false; + } else if (resultsByTerm.moreRemaining) { + return true; + } + return resultsByTerm.didInvalidate; +} + +export function searchIfNeeded(searchText: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (shouldSearch(getState(), searchText)) { + if (searchText.toLowerCase() === 'trending') { + return dispatch(searchCategory({tagName: searchText})); + } + return dispatch(searchGfycat({searchText})); + } + return Promise.resolve(); + }; +} + +export function searchIfNeededInitial(searchText: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch(updateSearchText(searchText)); + if (shouldSearchInitial(getState(), searchText)) { + if (searchText.toLowerCase() === 'trending') { + return dispatch(searchCategory({tagName: searchText})); + } + return dispatch(searchGfycat({searchText})); + } + return Promise.resolve(); + }; +} + +export function shouldSearchInitial(state: GlobalState, searchText: string) { + const resultsByTerm = state.entities.gifs.search.resultsByTerm[searchText]; + if (!resultsByTerm) { + return true; + } else if (resultsByTerm.isFetching) { + return false; + } + + return false; +} + +export function searchById(gfyId: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {GfycatApiKey, GfycatApiSecret} = getState().entities.general.config; + dispatch(requestSearchById(gfyId)); + return gfycatSdk(GfycatApiKey!, GfycatApiSecret!).searchById({id: gfyId}).then( + (response: any) => { + dispatch(receiveSearchById(gfyId, response.gfyItem)); + dispatch(cacheGifsRequest([response.gfyItem])); + } + ).catch((err: any) => dispatch(errorSearchById(err, gfyId))); + }; +} + +export function shouldSearchById(state: GlobalState, gfyId: string) { + return !state.entities.gifs.cache.gifs[gfyId]; //TODO investigate, used to be !state.cache.gifs[gfyId]; +} + +export function searchByIdIfNeeded(gfyId: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (shouldSearchById(getState(), gfyId)) { + return dispatch(searchById(gfyId)); + } + + return Promise.resolve(getState().entities.gifs.cache.gifs[gfyId]); //TODO: investigate, used to be getState().cache.gifs[gfyId] + }; +} + +export function saveSearchScrollPosition(scrollPosition: number) { + return (dispatch: DispatchFunc) => { + dispatch(searchScrollPosition(scrollPosition)); + }; +} + +export function saveSearchPriorLocation(priorLocation: number) { + return (dispatch: DispatchFunc) => { + dispatch(searchPriorLocation(priorLocation)); + }; +} + +export function searchTextUpdate(searchText: string) { + return (dispatch: DispatchFunc) => { + dispatch(updateSearchText(searchText)); + }; +} + +export function saveSearchBarText(searchBarText: string) { + return (dispatch: DispatchFunc) => { + dispatch(searchBarTextSave(searchBarText)); + }; +} + +// CATEGORIES + +export function categoriesListRequest() { + return { + type: GifTypes.REQUEST_CATEGORIES_LIST, + }; +} + +export function categoriesListReceived(json: any) { + return { + type: GifTypes.CATEGORIES_LIST_RECEIVED, + ...json, + }; +} + +export function categoriesListFailure(err: any) { + return { + type: GifTypes.CATEGORIES_LIST_FAILURE, + err, + }; +} + +export function requestCategoriesList({count = 60} = {}) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {GfycatApiKey, GfycatApiSecret} = getState().entities.general.config; + const state = getState().entities.gifs.categories; + if (!shouldRequestCategoriesList(state)) { + return Promise.resolve(); + } + dispatch(categoriesListRequest()); + const {cursor} = state; + const options = { + ...(count && {count}), + ...(cursor && {cursor}), + }; + return gfycatSdk(GfycatApiKey!, GfycatApiSecret!).getCategories(options).then((json: any) => { + const newGfycats = json.tags.reduce((gfycats: any[], tag: any) => { + if (tag.gfycats[0] && tag.gfycats[0].width) { + return [...gfycats, ...tag.gfycats]; + } + return gfycats; + }, []); + dispatch(cacheGifsRequest(newGfycats)); + dispatch(categoriesListReceived(json)); + }).catch( + (err: any) => { + dispatch(categoriesListFailure(err)); + } + ); + }; +} + +export function requestCategoriesListIfNeeded({ + count, +} = {count: undefined}) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState().entities.gifs.categories; + if (state.tagsList && state.tagsList.length) { + return Promise.resolve(); + } + return dispatch(requestCategoriesList({count})); + }; +} + +export function shouldRequestCategoriesList(state: {hasMore: boolean; isFetching: boolean; tagsList: any[]}) { + const {hasMore, isFetching, tagsList} = state; + if (!tagsList || !tagsList.length) { + return true; + } else if (isFetching) { + return false; + } else if (hasMore) { + return true; + } + return false; +} + +// CACHE + +export function cacheRequest() { + return { + type: GifTypes.CACHE_REQUEST, + payload: { + updating: true, + }, + }; +} + +export function cacheGifs(gifs: any) { + return { + type: GifTypes.CACHE_GIFS, + gifs, + }; +} + +export function cacheGifsRequest(gifs: any) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch(cacheRequest()); + dispatch(cacheGifs(gifs)); + return {data: true}; + }; +} diff --git a/app/mm-redux/actions/groups.test.js b/app/mm-redux/actions/groups.test.js new file mode 100644 index 000000000..185763c25 --- /dev/null +++ b/app/mm-redux/actions/groups.test.js @@ -0,0 +1,693 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/groups'; +import {Client4} from '@mm-redux/client'; + +import {RequestStatus, Groups} from '../constants'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.Groups', () => { + let store; + + beforeEach(async () => { + await TestHelper.initBasic(Client4); + store = await configureStore(); + }); + + afterEach(async () => { + await TestHelper.tearDown(); + }); + + it('getGroupSyncables', async () => { + const groupID = '5rgoajywb3nfbdtyafbod47rya'; + + const groupTeams = [ + { + team_id: 'ge63nq31sbfy3duzq5f7yqn1kh', + team_display_name: 'dolphins', + team_type: 'O', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542643748412, + delete_at: 0, + update_at: 1542643748412, + }, + { + team_id: 'tdjrcr3hg7yazyos17a53jduna', + team_display_name: 'developers', + team_type: 'O', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542643825026, + delete_at: 0, + update_at: 1542643825026, + }, + ]; + + const groupChannels = [ + { + channel_id: 'o3tdawqxot8kikzq8bk54zggbc', + channel_display_name: 'standup', + channel_type: 'P', + team_id: 'tdjrcr3hg7yazyos17a53jduna', + team_display_name: 'developers', + team_type: 'O', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542644105041, + delete_at: 0, + update_at: 1542644105041, + }, + { + channel_id: 's6oxu3embpdepyprx1fn5gjhea', + channel_display_name: 'swimming', + channel_type: 'P', + team_id: 'ge63nq31sbfy3duzq5f7yqn1kh', + team_display_name: 'dolphins', + team_type: 'O', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542644105042, + delete_at: 0, + update_at: 1542644105042, + }, + ]; + + nock(Client4.getBaseRoute()). + get(`/groups/${groupID}/teams`). + reply(200, groupTeams); + + nock(Client4.getBaseRoute()). + get(`/groups/${groupID}/channels`). + reply(200, groupChannels); + + await Actions.getGroupSyncables(groupID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState); + await Actions.getGroupSyncables(groupID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupSyncables = state.entities.groups.syncables[groupID]; + assert.ok(groupSyncables); + + for (let i = 0; i < 2; i++) { + assert.ok(JSON.stringify(groupSyncables.teams[i]) === JSON.stringify(groupTeams[i])); + assert.ok(JSON.stringify(groupSyncables.channels[i]) === JSON.stringify(groupChannels[i])); + } + }); + + it('getGroupMembers', async () => { + const groupID = '5rgoajywb3nfbdtyafbod47rya'; + + const response = { + members: [ + { + id: 'ok1mtgwrn7gbzetzfdgircykir', + create_at: 1542658437708, + update_at: 1542658441412, + delete_at: 0, + username: 'test.161927', + auth_data: 'test.161927', + auth_service: 'ldap', + email: 'success+test.161927@simulator.amazonses.com', + email_verified: true, + nickname: '', + first_name: 'test', + last_name: 'test.161927', + position: '', + roles: 'system_user', + notify_props: { + channel: 'true', + comments: 'never', + desktop: 'mention', + desktop_sound: 'true', + email: 'true', + first_name: 'false', + mention_keys: 'test.161927,@test.161927', + push: 'mention', + push_status: 'away', + }, + last_password_update: 1542658437708, + locale: 'en', + timezone: { + automaticTimezone: '', + manualTimezone: '', + useAutomaticTimezone: 'true', + }, + }, + ], + total_member_count: 1, + }; + + nock(Client4.getBaseRoute()). + get(`/groups/${groupID}/members?page=0&per_page=100`). + reply(200, response); + + await Actions.getGroupMembers(groupID, 0, 100)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupMembers = state.entities.groups.members; + assert.ok(groupMembers); + assert.ok(groupMembers[groupID].totalMemberCount === response.total_member_count); + + assert.ok(JSON.stringify(response.members[0]) === JSON.stringify(groupMembers[groupID].members[0])); + }); + + it('getGroup', async () => { + const groupID = '5rgoajywb3nfbdtyafbod47rya'; + + const response = { + id: '5rgoajywb3nfbdtyafbod47rya', + name: '8b7ks7ngqbgndqutka48gfzaqh', + display_name: 'Test Group 0', + description: '', + type: 'ldap', + remote_id: '\\eb\\80\\94\\cd\\d4\\32\\7c\\45\\87\\79\\1b\\fe\\45\\d9\\ac\\7b', + create_at: 1542399032816, + update_at: 1542399032816, + delete_at: 0, + has_syncables: false, + }; + + nock(Client4.getBaseRoute()). + get(`/groups/${groupID}`). + reply(200, response); + + await Actions.getGroup(groupID)(store.dispatch, store.getState); + + const state = store.getState(); + + const groups = state.entities.groups.groups; + assert.ok(groups); + assert.ok(groups[groupID]); + assert.ok(JSON.stringify(response) === JSON.stringify(groups[groupID])); + }); + + it('linkGroupSyncable', async () => { + const groupID = '5rgoajywb3nfbdtyafbod47rya'; + const teamID = 'ge63nq31sbfy3duzq5f7yqn1kh'; + const channelID = 'o3tdawqxot8kikzq8bk54zggbc'; + + const groupTeamResponse = { + team_id: 'ge63nq31sbfy3duzq5f7yqn1kh', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542643748412, + delete_at: 0, + update_at: 1542660566032, + }; + + const groupChannelResponse = { + channel_id: 'o3tdawqxot8kikzq8bk54zggbc', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542644105041, + delete_at: 0, + update_at: 1542662607342, + }; + + nock(Client4.getBaseRoute()). + post(`/groups/${groupID}/teams/${teamID}/link`). + reply(200, groupTeamResponse); + + nock(Client4.getBaseRoute()). + post(`/groups/${groupID}/channels/${channelID}/link`). + reply(200, groupChannelResponse); + + await Actions.linkGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState); + await Actions.linkGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState); + + const state = store.getState(); + const syncables = state.entities.groups.syncables; + assert.ok(syncables[groupID]); + + assert.ok(JSON.stringify(syncables[groupID].teams[0]) === JSON.stringify(groupTeamResponse)); + assert.ok(JSON.stringify(syncables[groupID].channels[0]) === JSON.stringify(groupChannelResponse)); + }); + + it('unlinkGroupSyncable', async () => { + const groupID = '5rgoajywb3nfbdtyafbod47rya'; + const teamID = 'ge63nq31sbfy3duzq5f7yqn1kh'; + const channelID = 'o3tdawqxot8kikzq8bk54zggbc'; + + const groupTeamResponse = { + team_id: 'ge63nq31sbfy3duzq5f7yqn1kh', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542643748412, + delete_at: 0, + update_at: 1542660566032, + }; + + const groupChannelResponse = { + channel_id: 'o3tdawqxot8kikzq8bk54zggbc', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + create_at: 1542644105041, + delete_at: 0, + update_at: 1542662607342, + }; + + nock(Client4.getBaseRoute()). + post(`/groups/${groupID}/teams/${teamID}/link`). + reply(200, groupTeamResponse); + + nock(Client4.getBaseRoute()). + post(`/groups/${groupID}/channels/${channelID}/link`). + reply(200, groupChannelResponse); + + await Actions.linkGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState); + await Actions.linkGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState); + + let state = store.getState(); + let syncables = state.entities.groups.syncables; + assert.ok(syncables[groupID]); + + assert.ok(JSON.stringify(syncables[groupID].teams[0]) === JSON.stringify(groupTeamResponse)); + assert.ok(JSON.stringify(syncables[groupID].channels[0]) === JSON.stringify(groupChannelResponse)); + + const beforeTeamsLength = syncables[groupID].teams.length; + const beforeChannelsLength = syncables[groupID].channels.length; + + nock(Client4.getBaseRoute()). + delete(`/groups/${groupID}/teams/${teamID}/link`). + reply(204, {ok: true}); + + nock(Client4.getBaseRoute()). + delete(`/groups/${groupID}/channels/${channelID}/link`). + reply(204, {ok: true}); + + await Actions.unlinkGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState); + await Actions.unlinkGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState); + + state = store.getState(); + syncables = state.entities.groups.syncables; + + assert.ok(syncables[groupID]); + assert.ok(syncables[groupID].teams.length === beforeTeamsLength - 1); + assert.ok(syncables[groupID].channels.length === beforeChannelsLength - 1); + }); + + it('getAllGroupsAssociatedToTeam', async () => { + const teamID = '5rgoajywb3nfbdtyafbod47ryb'; + + const response = { + groups: [ + { + id: 'xh585kyz3tn55q6ipfo57btwnc', + name: '9uobsi3xb3y5tfjb3ze7umnh1o', + display_name: 'abc', + description: '', + source: 'ldap', + remote_id: 'abc', + create_at: 1553808969975, + update_at: 1553808969975, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + { + id: 'tnd8zod9f3fdtqosxjmhwucbth', + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + remote_id: 'engineering', + create_at: 1553808971099, + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + { + id: 'qhdp6g7aubbpiyja7c4sgpe7tc', + name: 'x5bjwa4kwirpmqudhp5dterine', + display_name: 'qa', + description: '', + source: 'ldap', + remote_id: 'qa', + create_at: 1553808971548, + update_at: 1553808971548, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + ], + total_group_count: 3, + }; + + nock(Client4.getBaseRoute()). + get(`/teams/${teamID}/groups?paginate=false`). + reply(200, response); + + await Actions.getAllGroupsAssociatedToTeam(teamID)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupIDs = state.entities.teams.groupsAssociatedToTeam[teamID].ids; + assert.strictEqual(groupIDs.length, response.groups.length); + groupIDs.forEach((id) => { + assert.ok(response.groups.map((group) => group.id).includes(id)); + }); + }); + + it('getGroupsAssociatedToTeam', async () => { + const teamID = '5rgoajywb3nfbdtyafbod47ryb'; + + store = await configureStore({ + entities: { + teams: { + groupsAssociatedToTeam: { + [teamID]: ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc'], + }, + }, + }, + }); + + const response = { + groups: [ + { + id: 'tnd8zod9f3fdtqosxjmhwucbth', + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + remote_id: 'engineering', + create_at: 1553808971099, + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + { + id: 'qhdp6g7aubbpiyja7c4sgpe7tc', + name: 'x5bjwa4kwirpmqudhp5dterine', + display_name: 'qa', + description: '', + source: 'ldap', + remote_id: 'qa', + create_at: 1553808971548, + update_at: 1553808971548, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + ], + total_group_count: 3, + }; + + nock(Client4.getBaseRoute()). + get(`/teams/${teamID}/groups?page=100&per_page=60&q=0&include_member_count=true`). + reply(200, response); + + await Actions.getGroupsAssociatedToTeam(teamID, 0, 100)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupIDs = state.entities.teams.groupsAssociatedToTeam[teamID].ids; + const expectedIDs = ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc']; + assert.strictEqual(groupIDs.length, expectedIDs.length); + groupIDs.forEach((id) => { + assert.ok(expectedIDs.includes(id)); + assert.ok(state.entities.groups.groups[id]); + }); + + const count = state.entities.teams.groupsAssociatedToTeam[teamID].totalCount; + assert.equal(count, response.total_group_count); + }); + + it('getGroupsNotAssociatedToTeam', async () => { + const teamID = '5rgoajywb3nfbdtyafbod47ryb'; + + store = await configureStore({ + entities: { + teams: { + groupsAssociatedToTeam: { + [teamID]: {ids: ['existing1', 'existing2']}, + }, + }, + }, + }); + + const response = [ + { + id: 'existing1', + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + remote_id: 'engineering', + create_at: 1553808971099, + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + ]; + + nock(Client4.getBaseRoute()). + get(`/groups?not_associated_to_team=${teamID}&page=100&per_page=60&q=0&include_member_count=true`). + reply(200, response); + + await Actions.getGroupsNotAssociatedToTeam(teamID, 0, 100)(store.dispatch, store.getState); + + const state = store.getState(); + const groupIDs = state.entities.teams.groupsAssociatedToTeam[teamID].ids; + const expectedIDs = ['existing2'].concat(response.map((group) => group.id)); + assert.strictEqual(groupIDs.length, expectedIDs.length); + groupIDs.forEach((id) => { + assert.ok(expectedIDs.includes(id)); + }); + }); + + it('getAllGroupsAssociatedToChannel', async () => { + const channelID = '5rgoajywb3nfbdtyafbod47ryb'; + + const response = { + groups: [ + { + id: 'xh585kyz3tn55q6ipfo57btwnc', + name: '9uobsi3xb3y5tfjb3ze7umnh1o', + display_name: 'abc', + description: '', + source: 'ldap', + remote_id: 'abc', + create_at: 1553808969975, + update_at: 1553808969975, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + { + id: 'tnd8zod9f3fdtqosxjmhwucbth', + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + remote_id: 'engineering', + create_at: 1553808971099, + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + { + id: 'qhdp6g7aubbpiyja7c4sgpe7tc', + name: 'x5bjwa4kwirpmqudhp5dterine', + display_name: 'qa', + description: '', + source: 'ldap', + remote_id: 'qa', + create_at: 1553808971548, + update_at: 1553808971548, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + ], + total_group_count: 3, + }; + + nock(Client4.getBaseRoute()). + get(`/channels/${channelID}/groups?paginate=false`). + reply(200, response); + + await Actions.getAllGroupsAssociatedToChannel(channelID)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupIDs = state.entities.channels.groupsAssociatedToChannel[channelID].ids; + assert.strictEqual(groupIDs.length, response.groups.length); + groupIDs.forEach((id) => { + assert.ok(response.groups.map((group) => group.id).includes(id)); + }); + }); + + it('getGroupsAssociatedToChannel', async () => { + const channelID = '5rgoajywb3nfbdtyafbod47ryb'; + + store = await configureStore({ + entities: { + channels: { + groupsAssociatedToChannel: { + [channelID]: ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc'], + }, + }, + }, + }); + + const response = { + groups: [ + { + id: 'tnd8zod9f3fdtqosxjmhwucbth', + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + remote_id: 'engineering', + create_at: 1553808971099, + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + { + id: 'qhdp6g7aubbpiyja7c4sgpe7tc', + name: 'x5bjwa4kwirpmqudhp5dterine', + display_name: 'qa', + description: '', + source: 'ldap', + remote_id: 'qa', + create_at: 1553808971548, + update_at: 1553808971548, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + ], + total_group_count: 3, + }; + + nock(Client4.getBaseRoute()). + get(`/channels/${channelID}/groups?page=100&per_page=60&q=0&include_member_count=true`). + reply(200, response); + + await Actions.getGroupsAssociatedToChannel(channelID, 0, 100)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupIDs = state.entities.channels.groupsAssociatedToChannel[channelID].ids; + const expectedIDs = ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc']; + assert.strictEqual(groupIDs.length, expectedIDs.length); + groupIDs.forEach((id) => { + assert.ok(expectedIDs.includes(id)); + assert.ok(state.entities.groups.groups[id]); + }); + + const count = state.entities.channels.groupsAssociatedToChannel[channelID].totalCount; + assert.equal(count, response.total_group_count); + }); + + it('getGroupsNotAssociatedToChannel', async () => { + const channelID = '5rgoajywb3nfbdtyafbod47ryb'; + + store = await configureStore({ + entities: { + channels: { + groupsAssociatedToChannel: { + [channelID]: {ids: ['existing1', 'existing2']}, + }, + }, + }, + }); + + const response = [ + { + id: 'existing1', + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + remote_id: 'engineering', + create_at: 1553808971099, + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + ]; + + nock(Client4.getBaseRoute()). + get(`/groups?not_associated_to_channel=${channelID}&page=100&per_page=60&q=0&include_member_count=true`). + reply(200, response); + + await Actions.getGroupsNotAssociatedToChannel(channelID, 0, 100)(store.dispatch, store.getState); + + const state = store.getState(); + + const groupIDs = state.entities.channels.groupsAssociatedToChannel[channelID].ids; + const expectedIDs = ['existing2'].concat(response.map((group) => group.id)); + assert.strictEqual(groupIDs.length, expectedIDs.length); + groupIDs.forEach((id) => { + assert.ok(expectedIDs.includes(id)); + }); + }); + + it('patchGroupSyncable', async () => { + const groupID = '5rgoajywb3nfbdtyafbod47rya'; + const teamID = 'ge63nq31sbfy3duzq5f7yqn1kh'; + const channelID = 'o3tdawqxot8kikzq8bk54zggbc'; + + const groupSyncablePatch = { + auto_add: true, + scheme_admin: true, + }; + + const groupTeamResponse = { + team_id: 'ge63nq31sbfy3duzq5f7yqn1kh', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + scheme_admin: true, + create_at: 1542643748412, + delete_at: 0, + update_at: 1542660566032, + }; + + const groupChannelResponse = { + channel_id: 'o3tdawqxot8kikzq8bk54zggbc', + group_id: '5rgoajywb3nfbdtyafbod47rya', + auto_add: true, + scheme_admin: true, + create_at: 1542644105041, + delete_at: 0, + update_at: 1542662607342, + }; + + nock(Client4.getBaseRoute()). + put(`/groups/${groupID}/teams/${teamID}/patch`). + reply(200, groupTeamResponse); + + nock(Client4.getBaseRoute()). + put(`/groups/${groupID}/channels/${channelID}/patch`). + reply(200, groupChannelResponse); + + await Actions.patchGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM, groupSyncablePatch)(store.dispatch, store.getState); + await Actions.patchGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL, groupSyncablePatch)(store.dispatch, store.getState); + + const state = store.getState(); + const groupSyncables = state.entities.groups.syncables[groupID]; + assert.ok(groupSyncables); + + assert.ok(groupSyncables.teams[0].auto_add === groupSyncablePatch.auto_add); + assert.ok(groupSyncables.channels[0].auto_add === groupSyncablePatch.auto_add); + + assert.ok(groupSyncables.teams[0].scheme_admin === groupSyncablePatch.scheme_admin); + assert.ok(groupSyncables.channels[0].scheme_admin === groupSyncablePatch.scheme_admin); + }); +}); + diff --git a/app/mm-redux/actions/groups.ts b/app/mm-redux/actions/groups.ts new file mode 100644 index 000000000..268715fb8 --- /dev/null +++ b/app/mm-redux/actions/groups.ts @@ -0,0 +1,258 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {GroupTypes} from '@mm-redux/action_types'; +import {General, Groups} from '../constants'; +import {Client4} from '@mm-redux/client'; + +import {Action, ActionFunc, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; +import {SyncableType, SyncablePatch} from '@mm-redux/types/groups'; + +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; + +export function linkGroupSyncable(groupID: string, syncableID: string, syncableType: SyncableType, patch: SyncablePatch): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.linkGroupSyncable(groupID, syncableID, syncableType, patch); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const dispatches: Action[] = []; + + let type = ''; + switch (syncableType) { + case Groups.SYNCABLE_TYPE_TEAM: + dispatches.push({type: GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM, data: {teamID: syncableID, groups: [{id: groupID}]}}); + type = GroupTypes.LINKED_GROUP_TEAM; + break; + case Groups.SYNCABLE_TYPE_CHANNEL: + type = GroupTypes.LINKED_GROUP_CHANNEL; + break; + default: + console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console + } + + dispatches.push({type, data}); + dispatch(batchActions(dispatches)); + + return {data: true}; + }; +} + +export function unlinkGroupSyncable(groupID: string, syncableID: string, syncableType: SyncableType): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.unlinkGroupSyncable(groupID, syncableID, syncableType); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const dispatches: Action[] = []; + + let type = ''; + const data = {group_id: groupID, syncable_id: syncableID}; + switch (syncableType) { + case Groups.SYNCABLE_TYPE_TEAM: + type = GroupTypes.UNLINKED_GROUP_TEAM; + data.syncable_id = syncableID; + dispatches.push({type: GroupTypes.RECEIVED_GROUPS_NOT_ASSOCIATED_TO_TEAM, data: {teamID: syncableID, groups: [{id: groupID}]}}); + break; + case Groups.SYNCABLE_TYPE_CHANNEL: + type = GroupTypes.UNLINKED_GROUP_CHANNEL; + data.syncable_id = syncableID; + break; + default: + console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console + } + + dispatches.push({type, data}); + dispatch(batchActions(dispatches)); + + return {data: true}; + }; +} + +export function getGroupSyncables(groupID: string, syncableType: SyncableType): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getGroupSyncables(groupID, syncableType); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + let type = ''; + switch (syncableType) { + case Groups.SYNCABLE_TYPE_TEAM: + type = GroupTypes.RECEIVED_GROUP_TEAMS; + break; + case Groups.SYNCABLE_TYPE_CHANNEL: + type = GroupTypes.RECEIVED_GROUP_CHANNELS; + break; + default: + console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console + } + + dispatch(batchActions([ + {type, data, group_id: groupID}, + ])); + + return {data: true}; + }; +} + +export function patchGroupSyncable(groupID: string, syncableID: string, syncableType: SyncableType, patch: SyncablePatch): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.patchGroupSyncable(groupID, syncableID, syncableType, patch); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + return {error}; + } + + const dispatches: Action[] = []; + + let type = ''; + switch (syncableType) { + case Groups.SYNCABLE_TYPE_TEAM: + type = GroupTypes.PATCHED_GROUP_TEAM; + break; + case Groups.SYNCABLE_TYPE_CHANNEL: + type = GroupTypes.PATCHED_GROUP_CHANNEL; + break; + default: + console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console + } + + dispatches.push( + {type, data}, + ); + dispatch(batchActions(dispatches)); + + return {data: true}; + }; +} + +export function getGroupMembers(groupID: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getGroupMembers(groupID, page, perPage); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + {type: GroupTypes.RECEIVED_GROUP_MEMBERS, group_id: groupID, data}, + ])); + + return {data: true}; + }; +} + +export function getGroup(id: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getGroup, + onSuccess: [GroupTypes.RECEIVED_GROUP], + params: [ + id, + ], + }); +} + +export function getGroupsNotAssociatedToTeam(teamID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getGroupsNotAssociatedToTeam, + onSuccess: [GroupTypes.RECEIVED_GROUPS], + params: [ + teamID, + q, + page, + perPage, + ], + }); +} + +export function getGroupsNotAssociatedToChannel(channelID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getGroupsNotAssociatedToChannel, + onSuccess: [GroupTypes.RECEIVED_GROUPS], + params: [ + channelID, + q, + page, + perPage, + ], + }); +} + +export function getAllGroupsAssociatedToTeam(teamID: string): ActionFunc { + return bindClientFunc({ + clientFunc: async (param1) => { + const result = await Client4.getAllGroupsAssociatedToTeam(param1); + result.teamID = param1; + return result; + }, + onSuccess: [GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM], + params: [ + teamID, + ], + }); +} + +export function getAllGroupsAssociatedToChannel(channelID: string): ActionFunc { + return bindClientFunc({ + clientFunc: async (param1) => { + const result = await Client4.getAllGroupsAssociatedToChannel(param1); + result.channelID = param1; + return result; + }, + onSuccess: [GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNEL], + params: [ + channelID, + ], + }); +} + +export function getGroupsAssociatedToTeam(teamID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: async (param1, param2, param3, param4) => { + const result = await Client4.getGroupsAssociatedToTeam(param1, param2, param3, param4); + return {groups: result.groups, totalGroupCount: result.total_group_count, teamID: param1}; + }, + onSuccess: [GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM], + params: [ + teamID, + q, + page, + perPage, + ], + }); +} + +export function getGroupsAssociatedToChannel(channelID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: async (param1, param2, param3, param4) => { + const result = await Client4.getGroupsAssociatedToChannel(param1, param2, param3, param4); + return {groups: result.groups, totalGroupCount: result.total_group_count, channelID: param1}; + }, + onSuccess: [GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL], + params: [ + channelID, + q, + page, + perPage, + ], + }); +} diff --git a/app/mm-redux/actions/helpers.test.js b/app/mm-redux/actions/helpers.test.js new file mode 100644 index 000000000..d70a97c6a --- /dev/null +++ b/app/mm-redux/actions/helpers.test.js @@ -0,0 +1,129 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {UserTypes} from '@mm-redux/action_types'; +import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers'; +import {Client4} from '@mm-redux/client'; +import {ClientError} from '@mm-redux/client/client4'; +import configureStore, {mockDispatch} from 'test/test_store'; + +describe('Actions.Helpers', () => { + describe('forceLogoutIfNecessary', () => { + const token = 'token'; + + beforeEach(() => { + Client4.setToken(token); + }); + + it('should do nothing when passed a client error', async () => { + const store = await configureStore({ + entities: { + users: { + currentUserId: 'user', + }, + }, + }); + const dispatch = mockDispatch(store.dispatch); + + const error = new ClientError(Client4.getUrl(), { + message: 'no internet connection', + url: '/api/v4/foo/bar', + }); + + forceLogoutIfNecessary(error, dispatch, store.getState); + + assert.equal(Client4.token, token); + assert.deepEqual(dispatch.actions, []); + }); + + it('should do nothing when passed a non-401 server error', async () => { + const store = await configureStore({ + entities: { + users: { + currentUserId: 'user', + }, + }, + }); + const dispatch = mockDispatch(store.dispatch); + + const error = new ClientError(Client4.getUrl(), { + message: 'Failed to do something', + status_code: 403, + url: '/api/v4/foo/bar', + }); + + forceLogoutIfNecessary(error, dispatch, store.getState); + + assert.equal(Client4.token, token); + assert.deepEqual(dispatch.actions, []); + }); + + it('should trigger logout when passed a 401 server error', async () => { + const store = await configureStore({ + entities: { + users: { + currentUserId: 'user', + }, + }, + }); + const dispatch = mockDispatch(store.dispatch); + + const error = new ClientError(Client4.getUrl(), { + message: 'Failed to do something', + status_code: 401, + url: '/api/v4/foo/bar', + }); + + forceLogoutIfNecessary(error, dispatch, store.getState); + + assert.notEqual(Client4.token, token); + assert.deepEqual(dispatch.actions, [{type: UserTypes.LOGOUT_SUCCESS, data: {}}]); + }); + + it('should do nothing when failing to log in', async () => { + const store = await configureStore({ + entities: { + users: { + currentUserId: 'user', + }, + }, + }); + const dispatch = mockDispatch(store.dispatch); + + const error = new ClientError(Client4.getUrl(), { + message: 'Failed to do something', + status_code: 401, + url: '/api/v4/login', + }); + + forceLogoutIfNecessary(error, dispatch, store.getState); + + assert.equal(Client4.token, token); + assert.deepEqual(dispatch.actions, []); + }); + + it('should do nothing when not logged in', async () => { + const store = await configureStore({ + entities: { + users: { + currentUserId: '', + }, + }, + }); + const dispatch = mockDispatch(store.dispatch); + + const error = new ClientError(Client4.getUrl(), { + message: 'Failed to do something', + status_code: 401, + url: '/api/v4/foo/bar', + }); + + forceLogoutIfNecessary(error, dispatch, store.getState); + + assert.equal(Client4.token, token); + assert.deepEqual(dispatch.actions, []); + }); + }); +}); diff --git a/app/mm-redux/actions/helpers.ts b/app/mm-redux/actions/helpers.ts new file mode 100644 index 000000000..3f20887c1 --- /dev/null +++ b/app/mm-redux/actions/helpers.ts @@ -0,0 +1,151 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {UserTypes} from '@mm-redux/action_types'; + +import {Client4Error} from '@mm-redux/types/client4'; +import {batchActions, Action, ActionFunc, GenericAction, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; + +import {logError} from './errors'; +type ActionType = string; +const HTTP_UNAUTHORIZED = 401; +export function forceLogoutIfNecessary(err: Client4Error, dispatch: DispatchFunc, getState: GetStateFunc) { + const {currentUserId} = getState().entities.users; + + if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err.url && err.url.indexOf('/login') === -1 && currentUserId) { + Client4.setToken(''); + dispatch({type: UserTypes.LOGOUT_SUCCESS, data: {}}); + } +} + +function dispatcher(type: ActionType, data: any, dispatch: DispatchFunc, getState: GetStateFunc) { + if (type.indexOf('SUCCESS') === -1) { // we don't want to pass the data for the request types + dispatch(requestSuccess(type, data)); + } else { + dispatch(requestData(type)); + } +} + +export function requestData(type: ActionType): GenericAction { + return { + type, + data: null, + }; +} + +export function requestSuccess(type: ActionType, data: any) { + return { + type, + data, + }; +} + +export function requestFailure(type: ActionType, error: Client4Error): any { + return { + type, + error, + }; +} + +/** + * Returns an ActionFunc which calls a specfied (client) function and + * dispatches the specifed actions on request, success or failure. + * + * @export + * @param {Object} obj an object for destructirung required properties + * @param {() => Promise} obj.clientFunc clientFunc to execute + * @param {ActionType} obj.onRequest ActionType to dispatch on request + * @param {(ActionType | Array)} obj.onSuccess ActionType to dispatch on success + * @param {ActionType} obj.onFailure ActionType to dispatch on failure + * @param {...Array} obj.params + * @returns {ActionFunc} ActionFunc + */ + +export function bindClientFunc({ + clientFunc, + onRequest, + onSuccess, + onFailure, + params = [], +}: { + clientFunc: (...args: any[]) => Promise; + onRequest?: ActionType; + onSuccess?: ActionType | Array; + onFailure?: ActionType; + params?: Array; +}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (onRequest) { + dispatch(requestData(onRequest)); + } + + let data: any = null; + try { + data = await clientFunc(...params); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + const actions: Action[] = [logError(error)]; + if (onFailure) { + actions.push(requestFailure(onFailure, error)); + } + dispatch(batchActions(actions)); + return {error}; + } + + if (Array.isArray(onSuccess)) { + onSuccess.forEach((s) => { + dispatcher(s, data, dispatch, getState); + }); + } else if (onSuccess) { + dispatcher(onSuccess, data, dispatch, getState); + } + + return {data}; + }; +} + +// Debounce function based on underscores modified to use es6 and a cb + +export function debounce(func: (...args: any) => unknown, wait: number, immediate: boolean, cb: () => unknown) { + let timeout: NodeJS.Timeout|null; + return function fx(...args: Array) { + const runLater = () => { + timeout = null; + if (!immediate) { + Reflect.apply(func, this, args); + if (cb) { + cb(); + } + } + }; + const callNow = immediate && !timeout; + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(runLater, wait); + if (callNow) { + Reflect.apply(func, this, args); + if (cb) { + cb(); + } + } + }; +} + +export class FormattedError extends Error { + intl: { + id: string; + defaultMessage: string; + values: any; + }; + + constructor(id: string, defaultMessage: string, values: any = {}) { + super(defaultMessage); + this.intl = { + id, + defaultMessage, + values, + }; + } +} + diff --git a/app/mm-redux/actions/index.ts b/app/mm-redux/actions/index.ts new file mode 100644 index 000000000..ed26c3e87 --- /dev/null +++ b/app/mm-redux/actions/index.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as bots from './bots'; +import * as channels from './channels'; +import * as errors from './errors'; +import * as emojis from './emojis'; +import * as files from './files'; +import * as general from './general'; +import * as groups from './groups'; +import * as gifs from './gifs'; +import * as helpers from './helpers'; +import * as integrations from './integrations'; +import * as jobs from './jobs'; +import * as posts from './posts'; +import * as preferences from './preferences'; +import * as roles from './roles'; +import * as schemes from './schemes'; +import * as search from './search'; +import * as teams from './teams'; +import * as timezone from './timezone'; +import * as users from './users'; + +export { + bots, + channels, + errors, + emojis, + files, + general, + groups, + gifs, + integrations, + helpers, + jobs, + posts, + preferences, + roles, + schemes, + search, + teams, + timezone, + users, +}; + diff --git a/app/mm-redux/actions/integrations.test.js b/app/mm-redux/actions/integrations.test.js new file mode 100644 index 000000000..1595bb2d3 --- /dev/null +++ b/app/mm-redux/actions/integrations.test.js @@ -0,0 +1,775 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/integrations'; +import * as TeamsActions from '@mm-redux/actions/teams'; +import {Client4} from '@mm-redux/client'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Integrations', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('createIncomingHook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, TestHelper.testIncomingHook()); + + const {data: created} = await Actions.createIncomingHook( + { + channel_id: TestHelper.basicChannel.id, + display_name: 'test', + description: 'test', + }, + )(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.incomingHooks; + assert.ok(hooks); + assert.ok(hooks[created.id]); + }); + + it('getIncomingWebhook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, TestHelper.testIncomingHook()); + + const {data: created} = await Actions.createIncomingHook( + { + channel_id: TestHelper.basicChannel.id, + display_name: 'test', + description: 'test', + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/hooks/incoming/${created.id}`). + reply(200, created); + + await Actions.getIncomingHook(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.incomingHooks; + assert.ok(hooks); + assert.ok(hooks[created.id]); + }); + + it('getIncomingWebhooks', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, TestHelper.testIncomingHook()); + + const {data: created} = await Actions.createIncomingHook( + { + channel_id: TestHelper.basicChannel.id, + display_name: 'test', + description: 'test', + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/hooks/incoming'). + query(true). + reply(200, [created]); + + await Actions.getIncomingHooks(TestHelper.basicTeam.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.incomingHooks; + assert.ok(hooks); + assert.ok(hooks[created.id]); + }); + + it('removeIncomingHook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, TestHelper.testIncomingHook()); + + const {data: created} = await Actions.createIncomingHook( + { + channel_id: TestHelper.basicChannel.id, + display_name: 'test', + description: 'test', + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + delete(`/hooks/incoming/${created.id}`). + reply(200, OK_RESPONSE); + + await Actions.removeIncomingHook(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.incomingHooks; + assert.ok(!hooks[created.id]); + }); + + it('updateIncomingHook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/incoming'). + reply(201, TestHelper.testIncomingHook()); + + const {data: created} = await Actions.createIncomingHook( + { + channel_id: TestHelper.basicChannel.id, + display_name: 'test', + description: 'test', + }, + )(store.dispatch, store.getState); + + const updated = {...created}; + updated.display_name = 'test2'; + + nock(Client4.getBaseRoute()). + put(`/hooks/incoming/${created.id}`). + reply(200, updated); + await Actions.updateIncomingHook(updated)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.incomingHooks; + assert.ok(hooks[created.id]); + assert.ok(hooks[created.id].display_name === updated.display_name); + }); + + it('createOutgoingHook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, TestHelper.testOutgoingHook()); + + const {data: created} = await Actions.createOutgoingHook( + { + channel_id: TestHelper.basicChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'test', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://localhost/notarealendpoint'], + }, + )(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.outgoingHooks; + assert.ok(hooks); + assert.ok(hooks[created.id]); + }); + + it('getOutgoingWebhook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, TestHelper.testOutgoingHook()); + + const {data: created} = await Actions.createOutgoingHook( + { + channel_id: TestHelper.basicChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'test', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://localhost/notarealendpoint'], + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/hooks/outgoing/${created.id}`). + reply(200, TestHelper.testOutgoingHook()); + + await Actions.getOutgoingHook(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.outgoingHooks; + assert.ok(hooks); + assert.ok(hooks[created.id]); + }); + + it('getOutgoingWebhooks', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, TestHelper.testOutgoingHook()); + + const {data: created} = await Actions.createOutgoingHook( + { + channel_id: TestHelper.basicChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'test', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://localhost/notarealendpoint'], + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/hooks/outgoing'). + query(true). + reply(200, [TestHelper.testOutgoingHook()]); + + await Actions.getOutgoingHooks(TestHelper.basicChannel.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.outgoingHooks; + assert.ok(hooks); + assert.ok(hooks[created.id]); + }); + + it('removeOutgoingHook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, TestHelper.testOutgoingHook()); + + const {data: created} = await Actions.createOutgoingHook( + { + channel_id: TestHelper.basicChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'test', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://localhost/notarealendpoint'], + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + delete(`/hooks/outgoing/${created.id}`). + reply(200, OK_RESPONSE); + + await Actions.removeOutgoingHook(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.outgoingHooks; + assert.ok(!hooks[created.id]); + }); + + it('updateOutgoingHook', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, TestHelper.testOutgoingHook()); + + const {data: created} = await Actions.createOutgoingHook( + { + channel_id: TestHelper.basicChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'test', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://localhost/notarealendpoint'], + }, + )(store.dispatch, store.getState); + + const updated = {...created}; + updated.display_name = 'test2'; + nock(Client4.getBaseRoute()). + put(`/hooks/outgoing/${created.id}`). + reply(200, updated); + await Actions.updateOutgoingHook(updated)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.outgoingHooks; + assert.ok(hooks[created.id]); + assert.ok(hooks[created.id].display_name === updated.display_name); + }); + + it('regenOutgoingHookToken', async () => { + nock(Client4.getBaseRoute()). + post('/hooks/outgoing'). + reply(201, TestHelper.testOutgoingHook()); + + const {data: created} = await Actions.createOutgoingHook( + { + channel_id: TestHelper.basicChannel.id, + team_id: TestHelper.basicTeam.id, + display_name: 'test', + trigger_words: [TestHelper.generateId()], + callback_urls: ['http://localhost/notarealendpoint'], + }, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post(`/hooks/outgoing/${created.id}/regen_token`). + reply(200, {...created, token: TestHelper.generateId()}); + await Actions.regenOutgoingHookToken(created.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const hooks = state.entities.integrations.outgoingHooks; + assert.ok(hooks[created.id]); + assert.ok(hooks[created.id].token !== created.token); + }); + + it('getCommands', async () => { + const noTeamCommands = store.getState().entities.integrations.commands; + const noSystemCommands = store.getState().entities.integrations.systemCommands; + assert.equal(Object.keys({...noTeamCommands, ...noSystemCommands}).length, 0); + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const teamCommand = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...teamCommand, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: created} = await Actions.addCommand( + teamCommand, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/commands'). + query(true). + reply(200, [created, { + trigger: 'system-command', + }]); + + await Actions.getCommands( + team.id, + )(store.dispatch, store.getState); + + const teamCommands = store.getState().entities.integrations.commands; + const executableCommands = store.getState().entities.integrations.executableCommands; + assert.ok(Object.keys({...teamCommands, ...executableCommands}).length); + }); + + it('getAutocompleteCommands', async () => { + const noTeamCommands = store.getState().entities.integrations.commands; + const noSystemCommands = store.getState().entities.integrations.systemCommands; + assert.equal(Object.keys({...noTeamCommands, ...noSystemCommands}).length, 0); + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const teamCommandWithAutocomplete = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...teamCommandWithAutocomplete, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: createdWithAutocomplete} = await Actions.addCommand( + teamCommandWithAutocomplete, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/teams/${team.id}/commands/autocomplete`). + query(true). + reply(200, [createdWithAutocomplete, { + trigger: 'system-command', + }]); + + await Actions.getAutocompleteCommands( + team.id, + )(store.dispatch, store.getState); + + const teamCommands = store.getState().entities.integrations.commands; + const systemCommands = store.getState().entities.integrations.systemCommands; + assert.equal(Object.keys({...teamCommands, ...systemCommands}).length, 2); + }); + + it('getCustomTeamCommands', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/commands'). + query(true). + reply(200, []); + + await Actions.getCustomTeamCommands( + team.id, + )(store.dispatch, store.getState); + + const noCommands = store.getState().entities.integrations.commands; + assert.equal(Object.keys(noCommands).length, 0); + + const command = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...command, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: created} = await Actions.addCommand( + command, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/commands'). + query(true). + reply(200, []); + + await Actions.getCustomTeamCommands( + team.id, + )(store.dispatch, store.getState); + + const {commands} = store.getState().entities.integrations; + assert.ok(commands[created.id]); + assert.equal(Object.keys(commands).length, 1); + const actual = commands[created.id]; + const expected = created; + assert.equal(JSON.stringify(actual), JSON.stringify(expected)); + }); + + it('executeCommand', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const args = { + channel_id: TestHelper.basicChannel.id, + team_id: team.id, + }; + + nock(Client4.getBaseRoute()). + post('/commands/execute'). + reply(200, []); + + await Actions.executeCommand('/echo message 5', args); + }); + + it('addCommand', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const expected = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...expected, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: created} = await Actions.addCommand(expected)(store.dispatch, store.getState); + + const {commands} = store.getState().entities.integrations; + assert.ok(commands[created.id]); + const actual = commands[created.id]; + + assert.ok(actual.token); + assert.equal(actual.create_at, actual.update_at); + assert.equal(actual.delete_at, 0); + assert.ok(actual.creator_id); + assert.equal(actual.team_id, team.id); + assert.equal(actual.trigger, expected.trigger); + assert.equal(actual.method, expected.method); + assert.equal(actual.username, expected.username); + assert.equal(actual.icon_url, expected.icon_url); + assert.equal(actual.auto_complete, expected.auto_complete); + assert.equal(actual.auto_complete_desc, expected.auto_complete_desc); + assert.equal(actual.auto_complete_hint, expected.auto_complete_hint); + assert.equal(actual.display_name, expected.display_name); + assert.equal(actual.description, expected.description); + assert.equal(actual.url, expected.url); + }); + + it('regenCommandToken', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const command = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...command, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: created} = await Actions.addCommand( + command, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + put(`/commands/${created.id}/regen_token`). + reply(200, {...created, token: TestHelper.generateId()}); + + await Actions.regenCommandToken( + created.id, + )(store.dispatch, store.getState); + + const {commands} = store.getState().entities.integrations; + assert.ok(commands[created.id]); + const updated = commands[created.id]; + + assert.equal(updated.id, created.id); + assert.notEqual(updated.token, created.token); + assert.equal(updated.create_at, created.create_at); + assert.equal(updated.update_at, created.update_at); + assert.equal(updated.delete_at, created.delete_at); + assert.equal(updated.creator_id, created.creator_id); + assert.equal(updated.team_id, created.team_id); + assert.equal(updated.trigger, created.trigger); + assert.equal(updated.method, created.method); + assert.equal(updated.username, created.username); + assert.equal(updated.icon_url, created.icon_url); + assert.equal(updated.auto_complete, created.auto_complete); + assert.equal(updated.auto_complete_desc, created.auto_complete_desc); + assert.equal(updated.auto_complete_hint, created.auto_complete_hint); + assert.equal(updated.display_name, created.display_name); + assert.equal(updated.description, created.description); + assert.equal(updated.url, created.url); + }); + + it('editCommand', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const command = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...command, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: created} = await Actions.addCommand( + command, + )(store.dispatch, store.getState); + + const expected = Object.assign({}, created); + expected.trigger = 'modified'; + expected.method = 'G'; + expected.username = 'modified'; + expected.auto_complete = false; + + nock(Client4.getBaseRoute()). + put(`/commands/${expected.id}`). + reply(200, {...expected, update_at: 123}); + + await Actions.editCommand( + expected, + )(store.dispatch, store.getState); + + const {commands} = store.getState().entities.integrations; + assert.ok(commands[created.id]); + const actual = commands[created.id]; + + assert.notEqual(actual.update_at, expected.update_at); + expected.update_at = actual.update_at; + assert.equal(JSON.stringify(actual), JSON.stringify(expected)); + }); + + it('deleteCommand', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const {data: team} = await TeamsActions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const command = TestHelper.testCommand(team.id); + + nock(Client4.getBaseRoute()). + post('/commands'). + reply(201, {...command, token: TestHelper.generateId(), id: TestHelper.generateId()}); + + const {data: created} = await Actions.addCommand( + command, + )(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + delete(`/commands/${created.id}`). + reply(200, OK_RESPONSE); + + await Actions.deleteCommand( + created.id, + )(store.dispatch, store.getState); + + const {commands} = store.getState().entities.integrations; + assert.ok(!commands[created.id]); + }); + + it('addOAuthApp', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(oauthApps[created.id]); + }); + + it('getOAuthApp', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/oauth/apps/${created.id}`). + reply(200, created); + + await Actions.getOAuthApp(created.id)(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(oauthApps[created.id]); + }); + + it('editOAuthApp', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + const expected = Object.assign({}, created); + expected.name = 'modified'; + expected.description = 'modified'; + expected.homepage = 'https://modified.com'; + expected.icon_url = 'https://modified.com/icon'; + expected.callback_urls = ['https://modified.com/callback1', 'https://modified.com/callback2']; + expected.is_trusted = true; + + const nockReply = Object.assign({}, expected); + nockReply.update_at += 1; + nock(Client4.getBaseRoute()). + put(`/oauth/apps/${created.id}`).reply(200, nockReply); + + await Actions.editOAuthApp(expected)(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(oauthApps[created.id]); + + const actual = oauthApps[created.id]; + + assert.notEqual(actual.update_at, expected.update_at); + expected.update_at = actual.update_at; + assert.equal(JSON.stringify(actual), JSON.stringify(expected)); + }); + + it('getOAuthApps', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/oauth/apps'). + query(true). + reply(200, [created]); + + await Actions.getOAuthApps()(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(oauthApps); + }); + + it('getAuthorizedOAuthApps', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + const user = TestHelper.basicUser; + nock(Client4.getBaseRoute()). + get(`/users/${user.id}/oauth/apps/authorized`). + reply(200, [created]); + + await Actions.getAuthorizedOAuthApps()(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(oauthApps); + }); + + it('deleteOAuthApp', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + delete(`/oauth/apps/${created.id}`). + reply(200, OK_RESPONSE); + + await Actions.deleteOAuthApp(created.id)(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(!oauthApps[created.id]); + }); + + it('regenOAuthAppSecret', async () => { + nock(Client4.getBaseRoute()). + post('/oauth/apps'). + reply(201, TestHelper.fakeOAuthAppWithId()); + + const {data: created} = await Actions.addOAuthApp(TestHelper.fakeOAuthApp())(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post(`/oauth/apps/${created.id}/regen_secret`). + reply(200, {...created, client_secret: TestHelper.generateId()}); + + await Actions.regenOAuthAppSecret(created.id)(store.dispatch, store.getState); + + const {oauthApps} = store.getState().entities.integrations; + assert.ok(oauthApps[created.id].client_secret !== created.client_secret); + }); + + it('submitInteractiveDialog', async () => { + nock(Client4.getBaseRoute()). + post('/actions/dialogs/submit'). + reply(200, {errors: {name: 'some error'}}); + + const submit = { + url: 'https://mattermost.com', + callback_id: '123', + state: '123', + channel_id: TestHelper.generateId(), + team_id: TestHelper.generateId(), + submission: {name: 'value'}, + }; + + const {data} = await store.dispatch(Actions.submitInteractiveDialog(submit)); + + assert.ok(data.errors); + assert.equal(data.errors.name, 'some error'); + }); +}); diff --git a/app/mm-redux/actions/integrations.ts b/app/mm-redux/actions/integrations.ts new file mode 100644 index 000000000..9b2d47fe1 --- /dev/null +++ b/app/mm-redux/actions/integrations.ts @@ -0,0 +1,383 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {IntegrationTypes} from '@mm-redux/action_types'; +import {General} from '../constants'; +import {Client4} from '@mm-redux/client'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; + +import {batchActions, DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions'; + +import {Command, DialogSubmission, IncomingWebhook, OAuthApp, OutgoingWebhook} from '@mm-redux/types/integrations'; + +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +export function createIncomingHook(hook: IncomingWebhook): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createIncomingWebhook, + onSuccess: [IntegrationTypes.RECEIVED_INCOMING_HOOK], + params: [ + hook, + ], + }); +} + +export function getIncomingHook(hookId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getIncomingWebhook, + onSuccess: [IntegrationTypes.RECEIVED_INCOMING_HOOK], + params: [ + hookId, + ], + }); +} + +export function getIncomingHooks(teamId = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getIncomingWebhooks, + onSuccess: [IntegrationTypes.RECEIVED_INCOMING_HOOKS], + params: [ + teamId, + page, + perPage, + ], + }); +} + +export function removeIncomingHook(hookId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.removeIncomingWebhook(hookId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: IntegrationTypes.DELETED_INCOMING_HOOK, + data: {id: hookId}, + }, + ])); + + return {data: true}; + }; +} + +export function updateIncomingHook(hook: IncomingWebhook): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.updateIncomingWebhook, + onSuccess: [IntegrationTypes.RECEIVED_INCOMING_HOOK], + params: [ + hook, + ], + }); +} + +export function createOutgoingHook(hook: OutgoingWebhook): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createOutgoingWebhook, + onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_HOOK], + params: [ + hook, + ], + }); +} + +export function getOutgoingHook(hookId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getOutgoingWebhook, + onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_HOOK], + params: [ + hookId, + ], + }); +} + +export function getOutgoingHooks(channelId = '', teamId = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getOutgoingWebhooks, + onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_HOOKS], + params: [ + channelId, + teamId, + page, + perPage, + ], + }); +} + +export function removeOutgoingHook(hookId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.removeOutgoingWebhook(hookId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: IntegrationTypes.DELETED_OUTGOING_HOOK, + data: {id: hookId}, + }, + ])); + + return {data: true}; + }; +} + +export function updateOutgoingHook(hook: OutgoingWebhook): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.updateOutgoingWebhook, + onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_HOOK], + params: [ + hook, + ], + }); +} + +export function regenOutgoingHookToken(hookId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.regenOutgoingHookToken, + onSuccess: [IntegrationTypes.RECEIVED_OUTGOING_HOOK], + params: [ + hookId, + ], + }); +} + +export function getCommands(teamId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getCommandsList, + onSuccess: [IntegrationTypes.RECEIVED_COMMANDS], + params: [ + teamId, + ], + }); +} + +export function getAutocompleteCommands(teamId: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getAutocompleteCommandsList, + onSuccess: [IntegrationTypes.RECEIVED_COMMANDS], + params: [ + teamId, + page, + perPage, + ], + }); +} + +export function getCustomTeamCommands(teamId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getCustomTeamCommands, + onSuccess: [IntegrationTypes.RECEIVED_CUSTOM_TEAM_COMMANDS], + params: [ + teamId, + ], + }); +} + +export function addCommand(command: Command): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.addCommand, + onSuccess: [IntegrationTypes.RECEIVED_COMMAND], + params: [ + command, + ], + }); +} + +export function editCommand(command: Command): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.editCommand, + onSuccess: [IntegrationTypes.RECEIVED_COMMAND], + params: [ + command, + ], + }); +} + +export function executeCommand(command: Command, args: Array): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.executeCommand, + params: [ + command, + args, + ], + }); +} + +export function regenCommandToken(id: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let res; + try { + res = await Client4.regenCommandToken(id); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: IntegrationTypes.RECEIVED_COMMAND_TOKEN, + data: { + id, + token: res.token, + }, + }, + ])); + + return {data: true}; + }; +} + +export function deleteCommand(id: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.deleteCommand(id); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: IntegrationTypes.DELETED_COMMAND, + data: {id}, + }, + ])); + + return {data: true}; + }; +} + +export function addOAuthApp(app: OAuthApp): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createOAuthApp, + onSuccess: [IntegrationTypes.RECEIVED_OAUTH_APP], + params: [ + app, + ], + }); +} + +export function editOAuthApp(app: OAuthApp): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.editOAuthApp, + onSuccess: IntegrationTypes.RECEIVED_OAUTH_APP, + params: [ + app, + ], + }); +} + +export function getOAuthApps(page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getOAuthApps, + onSuccess: [IntegrationTypes.RECEIVED_OAUTH_APPS], + params: [ + page, + perPage, + ], + }); +} + +export function getOAuthApp(appId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getOAuthApp, + onSuccess: [IntegrationTypes.RECEIVED_OAUTH_APP], + params: [ + appId, + ], + }); +} + +export function getAuthorizedOAuthApps(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const currentUserId = getCurrentUserId(state); + + let data; + try { + data = await Client4.getAuthorizedOAuthApps(currentUserId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + + return {error}; + } + + return {data}; + }; +} + +export function deauthorizeOAuthApp(clientId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.deauthorizeOAuthApp, + params: [clientId], + }); +} + +export function deleteOAuthApp(id: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.deleteOAuthApp(id); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: IntegrationTypes.DELETED_OAUTH_APP, + data: {id}, + }, + ])); + + return {data: true}; + }; +} + +export function regenOAuthAppSecret(appId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.regenOAuthAppSecret, + onSuccess: [IntegrationTypes.RECEIVED_OAUTH_APP], + params: [ + appId, + ], + }); +} + +export function submitInteractiveDialog(submission: DialogSubmission): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + submission.channel_id = getCurrentChannelId(state); + submission.team_id = getCurrentTeamId(state); + + let data; + try { + data = await Client4.submitInteractiveDialog(submission); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + + dispatch(logError(error)); + return {error}; + } + + return {data}; + }; +} diff --git a/app/mm-redux/actions/jobs.test.js b/app/mm-redux/actions/jobs.test.js new file mode 100644 index 000000000..3bdd6c712 --- /dev/null +++ b/app/mm-redux/actions/jobs.test.js @@ -0,0 +1,145 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/jobs'; +import {Client4} from '@mm-redux/client'; + +import {RequestStatus} from '../constants'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Jobs', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('createJob', async () => { + const job = { + type: 'data_retention', + }; + + nock(Client4.getBaseRoute()). + post('/jobs'). + reply(201, { + id: 'six4h67ja7ntdkek6g13dp3wka', + create_at: 1491399241953, + type: 'data_retention', + status: 'pending', + data: {}, + }); + + await Actions.createJob(job)(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.jobs.createJob; + if (request.status === RequestStatus.FAILURE) { + throw new Error('createJob request failed'); + } + + const jobs = state.entities.jobs.jobs; + assert.ok(jobs.six4h67ja7ntdkek6g13dp3wka); + }); + + it('getJob', async () => { + nock(Client4.getBaseRoute()). + get('/jobs/six4h67ja7ntdkek6g13dp3wka'). + reply(200, { + id: 'six4h67ja7ntdkek6g13dp3wka', + create_at: 1491399241953, + type: 'data_retention', + status: 'pending', + data: {}, + }); + + await Actions.getJob('six4h67ja7ntdkek6g13dp3wka')(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.jobs.getJob; + if (request.status === RequestStatus.FAILURE) { + throw new Error('getJob request failed'); + } + + const jobs = state.entities.jobs.jobs; + assert.ok(jobs.six4h67ja7ntdkek6g13dp3wka); + }); + + it('cancelJob', async () => { + nock(Client4.getBaseRoute()). + post('/jobs/six4h67ja7ntdkek6g13dp3wka/cancel'). + reply(200, OK_RESPONSE); + + await Actions.cancelJob('six4h67ja7ntdkek6g13dp3wka')(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.jobs.cancelJob; + if (request.status === RequestStatus.FAILURE) { + throw new Error('cancelJob request failed'); + } + }); + + it('getJobs', async () => { + nock(Client4.getBaseRoute()). + get('/jobs'). + query(true). + reply(200, [{ + id: 'six4h67ja7ntdkek6g13dp3wka', + create_at: 1491399241953, + type: 'data_retention', + status: 'pending', + data: {}, + }]); + + await Actions.getJobs()(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.jobs.getJobs; + if (request.status === RequestStatus.FAILURE) { + throw new Error('getJobs request failed'); + } + + const jobs = state.entities.jobs.jobs; + assert.ok(jobs.six4h67ja7ntdkek6g13dp3wka); + }); + + it('getJobsByType', async () => { + nock(Client4.getBaseRoute()). + get('/jobs/type/data_retention'). + query(true). + reply(200, [{ + id: 'six4h67ja7ntdkek6g13dp3wka', + create_at: 1491399241953, + type: 'data_retention', + status: 'pending', + data: {}, + }]); + + await Actions.getJobsByType('data_retention')(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.jobs.getJobs; + if (request.status === RequestStatus.FAILURE) { + throw new Error('getJobsByType request failed'); + } + + const jobs = state.entities.jobs.jobs; + assert.ok(jobs.six4h67ja7ntdkek6g13dp3wka); + + const jobsByType = state.entities.jobs.jobsByTypeList; + assert.ok(jobsByType.data_retention); + assert.ok(jobsByType.data_retention.length === 1); + }); +}); diff --git a/app/mm-redux/actions/jobs.ts b/app/mm-redux/actions/jobs.ts new file mode 100644 index 000000000..15823b779 --- /dev/null +++ b/app/mm-redux/actions/jobs.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {JobTypes} from '@mm-redux/action_types'; +import {Client4} from '@mm-redux/client'; +import {General} from '../constants'; + +import {ActionFunc} from '@mm-redux/types/actions'; +import {JobType, Job} from '@mm-redux/types/jobs'; + +import {bindClientFunc} from './helpers'; +export function createJob(job: Job): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createJob, + onRequest: JobTypes.CREATE_JOB_REQUEST, + onSuccess: [JobTypes.RECEIVED_JOB, JobTypes.CREATE_JOB_SUCCESS], + onFailure: JobTypes.CREATE_JOB_FAILURE, + params: [ + job, + ], + }); +} + +export function getJob(id: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getJob, + onRequest: JobTypes.GET_JOB_REQUEST, + onSuccess: [JobTypes.RECEIVED_JOB, JobTypes.GET_JOB_SUCCESS], + onFailure: JobTypes.GET_JOB_FAILURE, + params: [ + id, + ], + }); +} + +export function getJobs(page = 0, perPage: number = General.JOBS_CHUNK_SIZE): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getJobs, + onRequest: JobTypes.GET_JOBS_REQUEST, + onSuccess: [JobTypes.RECEIVED_JOBS, JobTypes.GET_JOBS_SUCCESS], + onFailure: JobTypes.GET_JOBS_FAILURE, + params: [ + page, + perPage, + ], + }); +} + +export function getJobsByType(type: JobType, page = 0, perPage: number = General.JOBS_CHUNK_SIZE): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getJobsByType, + onRequest: JobTypes.GET_JOBS_REQUEST, + onSuccess: [JobTypes.RECEIVED_JOBS, JobTypes.RECEIVED_JOBS_BY_TYPE, JobTypes.GET_JOBS_SUCCESS], + onFailure: JobTypes.GET_JOBS_FAILURE, + params: [ + type, + page, + perPage, + ], + }); +} + +export function cancelJob(job: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.cancelJob, + onRequest: JobTypes.CANCEL_JOB_REQUEST, + onSuccess: JobTypes.CANCEL_JOB_SUCCESS, + onFailure: JobTypes.CANCEL_JOB_FAILURE, + params: [ + job, + ], + }); +} diff --git a/app/mm-redux/actions/posts.test.js b/app/mm-redux/actions/posts.test.js new file mode 100644 index 000000000..e4320b844 --- /dev/null +++ b/app/mm-redux/actions/posts.test.js @@ -0,0 +1,1930 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'fs'; +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/posts'; +import {getChannelStats} from '@mm-redux/actions/channels'; +import {login} from '@mm-redux/actions/users'; +import {setSystemEmojis, createCustomEmoji} from '@mm-redux/actions/emojis'; +import {Client4} from '@mm-redux/client'; +import {Preferences, Posts, RequestStatus} from '../constants'; +import {ChannelTypes, PostTypes} from '@mm-redux/action_types'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Posts', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('createPost', async () => { + const channelId = TestHelper.basicChannel.id; + const post = TestHelper.fakePost(channelId); + + nock(Client4.getBaseRoute()). + post('/posts'). + reply(201, {...post, id: TestHelper.generateId()}); + + await Actions.createPost(post)(store.dispatch, store.getState); + + const state = store.getState(); + const createRequest = state.requests.posts.createPost; + if (createRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(createRequest.error)); + } + + const {posts, postsInChannel} = state.entities.posts; + assert.ok(posts); + assert.ok(postsInChannel); + + let found = false; + for (const storedPost of Object.values(posts)) { + if (storedPost.message === post.message) { + found = true; + break; + } + } + assert.ok(found, 'failed to find new post in posts'); + + // postsInChannel[channelId] should not exist as create post should not add entry to postsInChannel when it did not exist before + assert.ok(!postsInChannel[channelId], 'postIds in channel do not exist'); + }); + + // it('resetCreatePostRequest', async () => { + // const channelId = TestHelper.basicChannel.id; + // const post = TestHelper.fakePost(channelId); + // const createPostError = { + // message: 'Invalid RootId parameter', + // server_error_id: 'api.post.create_post.root_id.app_error', + // status_code: 400, + // url: 'http://localhost:8065/api/v4/posts', + // }; + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(400, createPostError); + + // await Actions.createPost(post)(store.dispatch, store.getState); + // await TestHelper.wait(50); + + // let state = store.getState(); + // let createRequest = state.requests.posts.createPost; + // if (createRequest.status !== RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(createRequest.error)); + // } + + // assert.equal(createRequest.status, RequestStatus.FAILURE); + // assert.equal(createRequest.error.message, createPostError.message); + // assert.equal(createRequest.error.status_code, createPostError.status_code); + + // store.dispatch(Actions.resetCreatePostRequest()); + // await TestHelper.wait(50); + + // state = store.getState(); + // createRequest = state.requests.posts.createPost; + // if (createRequest.status === RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(createRequest.error)); + // } + + // assert.equal(createRequest.status, RequestStatus.NOT_STARTED); + // assert.equal(createRequest.error, null); + // }); + + // it('createPost with file attachments', async () => { + // const channelId = TestHelper.basicChannel.id; + // const post = TestHelper.fakePost(channelId); + // const files = TestHelper.fakeFiles(3); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, {...post, id: TestHelper.generateId(), file_ids: [files[0].id, files[1].id, files[2].id]}); + + // await Actions.createPost( + // post, + // files, + // )(store.dispatch, store.getState); + + // const state = store.getState(); + // const createRequest = state.requests.posts.createPost; + // if (createRequest.status === RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(createRequest.error)); + // } + + // let newPost; + // for (const storedPost of Object.values(state.entities.posts.posts)) { + // if (storedPost.message === post.message) { + // newPost = storedPost; + // break; + // } + // } + // assert.ok(newPost, 'failed to find new post in posts'); + + // let found = true; + // for (const file of files) { + // if (!state.entities.files.files[file.id]) { + // found = false; + // break; + // } + // } + // assert.ok(found, 'failed to find uploaded files in files'); + + // const postIdForFiles = state.entities.files.fileIdsByPostId[newPost.id]; + // assert.ok(postIdForFiles, 'failed to find files for post id in files Ids by post id'); + + // assert.equal(postIdForFiles.length, files.length); + // }); + + // // it('retry failed post', async () => { + // // const channelId = TestHelper.basicChannel.id; + // // const post = TestHelper.fakePost(channelId); + + // // nock(Client4.getBaseRoute()). + // // post('/posts'). + // // reply(400, {}); + + // // nock(Client4.getBaseRoute()). + // // post(''). + // // reply(201, {...post, id: TestHelper.generateId()}); + + // // await Actions.createPost(post)(store.dispatch, store.getState); + + // // await TestHelper.wait(200); + + // // let state = store.getState(); + + // // const {posts} = state.entities.posts; + // // assert.ok(posts); + + // // let failedPost; + // // for (const storedPost of Object.values(posts)) { + // // if (storedPost.failed) { + // // failedPost = storedPost; + // // break; + // // } + // // } + + // // assert.ok(failedPost, 'failed to find failed post'); + + // // // Retry the post + // // const {id, failed, ...retryPost} = failedPost; // eslint-disable-line + // // await Actions.createPost(retryPost)(store.dispatch, store.getState); + + // // await TestHelper.wait(500); + + // // state = store.getState(); + // // const {posts: nextPosts} = state.entities.posts; + + // // let found = false; + // // for (const storedPost of Object.values(nextPosts)) { + // // if (storedPost.pending_post_id === failedPost.pending_post_id) { + // // if (!storedPost.failed) { + // // found = true; + // // break; + // // } + // // } + // // } + + // // assert.ok(found, 'Retried post failed again.'); + // // }); + + // it('editPost', async () => { + // const channelId = TestHelper.basicChannel.id; + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(channelId)); + + // const post = await Client4.createPost( + // TestHelper.fakePost(channelId), + // ); + // const message = post.message; + + // post.message = `${message} (edited)`; + + // nock(Client4.getBaseRoute()). + // put(`/posts/${post.id}/patch`). + // reply(200, post); + + // await Actions.editPost( + // post, + // )(store.dispatch, store.getState); + + // const state = store.getState(); + // const editRequest = state.requests.posts.editPost; + // const {posts} = state.entities.posts; + + // if (editRequest.status === RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(editRequest.error)); + // } + + // assert.ok(posts); + // assert.ok(posts[post.id]); + + // assert.strictEqual( + // posts[post.id].message, + // `${message} (edited)`, + // ); + // }); + + // it('deletePost', async () => { + // const channelId = TestHelper.basicChannel.id; + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(channelId)); + // await Actions.createPost(TestHelper.fakePost(channelId))(store.dispatch, store.getState); + // const initialPosts = store.getState().entities.posts; + // const postId = Object.keys(initialPosts.posts)[0]; + // await Actions.deletePost(initialPosts.posts[postId])(store.dispatch, store.getState); + + // const state = store.getState(); + // const {posts} = state.entities.posts; + + // assert.ok(posts); + // assert.ok(posts[postId]); + // assert.strictEqual( + // posts[postId].state, + // Posts.POST_DELETED, + // ); + // }); + + // it('deletePostWithReaction', async () => { + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const emojiName = '+1'; + + // nock(Client4.getBaseRoute()). + // post('/reactions'). + // reply(201, {user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName, create_at: 1508168444721}); + // await Actions.addReaction(post1.id, emojiName)(store.dispatch, store.getState); + + // let reactions = store.getState().entities.posts.reactions; + // assert.ok(reactions); + // assert.ok(reactions[post1.id]); + // assert.ok(reactions[post1.id][TestHelper.basicUser.id + '-' + emojiName]); + + // await Actions.deletePost(post1)(store.dispatch, store.getState); + + // reactions = store.getState().entities.posts.reactions; + // assert.ok(reactions); + // assert.ok(!reactions[post1.id]); + // }); + + // it('removePost', async () => { + // const post1 = {id: 'post1', channel_id: 'channel1', create_at: 1001, message: ''}; + // const post2 = {id: 'post2', channel_id: 'channel1', create_at: 1002, message: '', is_pinned: true}; + // const post3 = {id: 'post3', channel_id: 'channel1', root_id: 'post2', create_at: 1003, message: ''}; + // const post4 = {id: 'post4', channel_id: 'channel1', root_id: 'post1', create_at: 1004, message: ''}; + + // store = await configureStore({ + // entities: { + // posts: { + // posts: { + // post1, + // post2, + // post3, + // post4, + // }, + // postsInChannel: { + // channel1: [ + // {order: ['post4', 'post3', 'post2', 'post1'], recent: false}, + // ], + // }, + // postsInThread: { + // post1: ['post4'], + // post2: ['post3'], + // }, + // }, + // channels: { + // stats: { + // channel1: { + // pinnedpost_count: 2, + // }, + // }, + // }, + // }, + // }); + + // await store.dispatch(Actions.removePost(post2)); + + // const state = store.getState(); + // const {stats} = state.entities.channels; + // const pinned_post_count = stats.channel1.pinnedpost_count; + + // expect(state.entities.posts.posts).toEqual({ + // post1, + // post4, + // }); + // expect(state.entities.posts.postsInChannel).toEqual({ + // channel1: [ + // {order: ['post4', 'post1'], recent: false}, + // ], + // }); + // expect(state.entities.posts.postsInThread).toEqual({ + // post1: ['post4'], + // }); + // expect(pinned_post_count).toEqual(1); + // }); + + // it('removePostWithReaction', async () => { + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const emojiName = '+1'; + + // nock(Client4.getBaseRoute()). + // post('/reactions'). + // reply(201, {user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName, create_at: 1508168444721}); + // await Actions.addReaction(post1.id, emojiName)(store.dispatch, store.getState); + + // let reactions = store.getState().entities.posts.reactions; + // assert.ok(reactions); + // assert.ok(reactions[post1.id]); + // assert.ok(reactions[post1.id][TestHelper.basicUser.id + '-' + emojiName]); + + // await store.dispatch(Actions.removePost(post1)); + + // reactions = store.getState().entities.posts.reactions; + // assert.ok(reactions); + // assert.ok(!reactions[post1.id]); + // }); + + // it('getPostsUnread', async () => { + // const {dispatch, getState} = store; + // const channelId = TestHelper.basicChannel.id; + // const post = TestHelper.fakePostWithId(channelId); + // const userId = getState().entities.users.currentUserId; + // const response = { + // posts: { + // [post.id]: post, + // }, + // order: [post.id], + // next_post_id: '', + // prev_post_id: '', + // }; + + // nock(Client4.getUsersRoute()). + // get(`/${userId}/channels/${channelId}/posts/unread`). + // query(true). + // reply(200, response); + + // await Actions.getPostsUnread(channelId)(dispatch, getState); + // const {posts} = getState().entities.posts; + + // assert.ok(posts[post.id]); + // }); + + // it('getPostThread', async () => { + // const channelId = TestHelper.basicChannel.id; + // const post = {id: TestHelper.generateId(), channel_id: channelId, message: ''}; + // const comment = {id: TestHelper.generateId(), root_id: post.id, channel_id: channelId, message: ''}; + + // store.dispatch(Actions.receivedPostsInChannel({order: [post.id], posts: {[post.id]: post}}, channelId)); + + // const postList = { + // order: [post.id], + // posts: { + // [post.id]: post, + // [comment.id]: comment, + // }, + // }; + + // nock(Client4.getBaseRoute()). + // get(`/posts/${post.id}/thread`). + // reply(200, postList); + // await Actions.getPostThread(post.id)(store.dispatch, store.getState); + + // const state = store.getState(); + // const getRequest = state.requests.posts.getPostThread; + // const { + // posts, + // postsInChannel, + // postsInThread, + // } = state.entities.posts; + + // if (getRequest.status === RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(getRequest.error)); + // } + + // assert.ok(posts); + // assert.ok(posts[post.id]); + // assert.ok(postsInThread[post.id]); + // assert.deepEqual(postsInThread[post.id], [comment.id]); + // assert.ok(postsInChannel[channelId]); + + // const found = postsInChannel[channelId].find((block) => block.order.indexOf(comment.id) !== -1); + // assert.ok(!found, 'should not have found comment in postsInChannel'); + // }); + + // it('getPosts', async () => { + // const post0 = {id: 'post0', channel_id: 'channel1', create_at: 1000, message: ''}; + // const post1 = {id: 'post1', channel_id: 'channel1', create_at: 1001, message: ''}; + // const post2 = {id: 'post2', channel_id: 'channel1', create_at: 1002, message: ''}; + // const post3 = {id: 'post3', channel_id: 'channel1', root_id: 'post2', create_at: 1003, message: ''}; + // const post4 = {id: 'post4', channel_id: 'channel1', root_id: 'post0', create_at: 1004, message: ''}; + + // const postList = { + // order: ['post4', 'post3', 'post2', 'post1'], + // posts: { + // post0, + // post1, + // post2, + // post3, + // post4, + // }, + // }; + + // nock(Client4.getChannelsRoute()). + // get('/channel1/posts'). + // query(true). + // reply(200, postList); + + // const result = await store.dispatch(Actions.getPosts('channel1')); + + // expect(result).toEqual({data: postList}); + + // const state = store.getState(); + + // expect(state.entities.posts.posts).toEqual({ + // post0, + // post1, + // post2, + // post3, + // post4, + // }); + // expect(state.entities.posts.postsInChannel).toEqual({ + // channel1: [ + // {order: ['post4', 'post3', 'post2', 'post1'], recent: true, oldest: false}, + // ], + // }); + // expect(state.entities.posts.postsInThread).toEqual({ + // post0: ['post4'], + // post2: ['post3'], + // }); + // }); + + // it('getNeededAtMentionedUsernames', async () => { + // const state = { + // entities: { + // users: { + // profiles: { + // 1: { + // id: '1', + // username: 'aaa', + // }, + // }, + // }, + // }, + // }; + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: 'aaa'}, + // ]), + // new Set(), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '@aaa'}, + // ]), + // new Set(), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '@aaa @bbb @ccc'}, + // ]), + // new Set(['bbb', 'ccc']), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '@bbb. @ccc.ddd'}, + // ]), + // new Set(['bbb.', 'bbb', 'ccc.ddd']), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '@bbb- @ccc-ddd'}, + // ]), + // new Set(['bbb-', 'bbb', 'ccc-ddd']), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '@bbb_ @ccc_ddd'}, + // ]), + // new Set(['bbb_', 'ccc_ddd']), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '(@bbb/@ccc) ddd@eee'}, + // ]), + // new Set(['bbb', 'ccc']), + // ); + + // assert.deepEqual( + // Actions.getNeededAtMentionedUsernames(state, [ + // {message: '@all'}, + // {message: '@here'}, + // {message: '@channel'}, + // {message: '@all.'}, + // {message: '@here.'}, + // {message: '@channel.'}, + // ]), + // new Set(), + // 'should never try to request usernames matching special mentions', + // ); + // }); + + // describe('getNeededCustomEmojis', () => { + // const state = { + // entities: { + // emojis: { + // customEmoji: { + // 1: { + // id: '1', + // creator_id: '1', + // name: 'name1', + // }, + // }, + // nonExistentEmoji: new Set(['name2']), + // }, + // general: { + // config: { + // EnableCustomEmoji: 'true', + // }, + // }, + // }, + // }; + + // setSystemEmojis(new Map([['systemEmoji1', {}]])); + + // it('no emojis in post', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: 'aaa'}, + // ]), + // new Set(), + // ); + // }); + + // it('already loaded custom emoji in post', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':name1:'}, + // ]), + // new Set(), + // ); + // }); + + // it('system emoji in post', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':systemEmoji1:'}, + // ]), + // new Set(), + // ); + // }); + + // it('mixed emojis in post', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':systemEmoji1: :name1: :name2: :name3:'}, + // ]), + // new Set(['name3']), + // ); + // }); + + // it('custom emojis and text in post', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: 'aaa :name3: :name4:'}, + // ]), + // new Set(['name3', 'name4']), + // ); + // }); + + // it('custom emoji followed by punctuation', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':name3:!'}, + // ]), + // new Set(['name3']), + // ); + // }); + + // it('custom emoji including hyphen', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':name-3:'}, + // ]), + // new Set(['name-3']), + // ); + // }); + + // it('custom emoji including underscore', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':name_3:'}, + // ]), + // new Set(['name_3']), + // ); + // }); + + // it('custom emoji in message attachment text', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: [{text: ':name3:'}]}}, + // ]), + // new Set(['name3']), + // ); + // }); + + // it('custom emoji in message attachment pretext', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: [{pretext: ':name3:'}]}}, + // ]), + // new Set(['name3']), + // ); + // }); + + // it('custom emoji in message attachment field', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: [{fields: [{value: ':name3:'}]}]}}, + // ]), + // new Set(['name3']), + // ); + // }); + + // it('mixed emojis in message attachment', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: [{text: ':name4: :name1:', pretext: ':name3: :systemEmoji1:', fields: [{value: ':name3:'}]}]}}, + // ]), + // new Set(['name3', 'name4']), + // ); + // }); + + // it('empty message attachment field', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: [{fields: [{}]}]}}, + // ]), + // new Set([]), + // ); + // }); + + // it('null message attachment contents', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: [{text: null, pretext: null, fields: null}]}}, + // ]), + // new Set([]), + // ); + // }); + + // it('null message attachment', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: '', props: {attachments: null}}, + // ]), + // new Set([]), + // ); + // }); + + // it('multiple posts', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // {message: ':emoji3:'}, + // {message: ':emoji4:'}, + // ]), + // new Set(['emoji3', 'emoji4']), + // ); + // }); + + // it('with custom emojis disabled', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis({ + // entities: { + // ...state.entities, + // general: { + // config: { + // EnableCustomEmoji: 'false', + // }, + // }, + // }, + // }, [ + // {message: ':emoji3:'}, + // ]), + // new Set([]), + // ); + // }); + + // it('do not load emojis when the post has metadata', () => { + // assert.deepEqual( + // Actions.getNeededCustomEmojis(state, [ + // { + // message: ':emoji3:', + // metadata: { + // emojis: [{name: 'emoji3'}], + // }, + // }, + // ]), + // new Set([]), + // ); + // }); + // }); + + // it('getPostsSince', async () => { + // const post0 = {id: 'post0', channel_id: 'channel1', create_at: 1000, message: ''}; + // const post1 = {id: 'post1', channel_id: 'channel1', create_at: 1001, message: ''}; + // const post2 = {id: 'post2', channel_id: 'channel1', create_at: 1002, message: ''}; + // const post3 = {id: 'post3', channel_id: 'channel1', create_at: 1003, message: ''}; + // const post4 = {id: 'post4', channel_id: 'channel1', root_id: 'post0', create_at: 1004, message: ''}; + + // store = await configureStore({ + // entities: { + // posts: { + // posts: { + // post1, + // post2, + // }, + // postsInChannel: { + // channel1: [ + // {order: ['post2', 'post1'], recent: true}, + // ], + // }, + // }, + // }, + // }); + + // const postList = { + // order: ['post4', 'post3', 'post1'], + // posts: { + // post0, + // post1, // Pretend post1 has been updated + // post3, + // post4, + // }, + // }; + + // nock(Client4.getChannelsRoute()). + // get('/channel1/posts'). + // query(true). + // reply(200, postList); + + // const result = await store.dispatch(Actions.getPostsSince('channel1', post2.create_at)); + + // expect(result).toEqual({data: postList}); + + // const state = store.getState(); + + // expect(state.entities.posts.posts).toEqual({ + // post0, + // post1, + // post2, + // post3, + // post4, + // }); + // expect(state.entities.posts.postsInChannel).toEqual({ + // channel1: [ + // {order: ['post4', 'post3', 'post2', 'post1'], recent: true}, + // ], + // }); + // expect(state.entities.posts.postsInThread).toEqual({ + // post0: ['post4'], + // }); + // }); + + // it('getPostsBefore', async () => { + // const channelId = 'channel1'; + + // const post1 = {id: 'post1', channel_id: channelId, create_at: 1001, message: ''}; + // const post2 = {id: 'post2', channel_id: channelId, root_id: 'post1', create_at: 1002, message: ''}; + // const post3 = {id: 'post3', channel_id: channelId, create_at: 1003, message: ''}; + + // store = await configureStore({ + // entities: { + // posts: { + // posts: { + // post3, + // }, + // postsInChannel: { + // channel1: [ + // {order: ['post1'], recent: false, oldest: false}, + // ], + // }, + // }, + // }, + // }); + + // const postList = { + // order: [post2.id, post1.id], + // posts: { + // post2, + // post1, + // }, + // prev_post_id: '', + // next_post_id: 'post3', + // }; + + // nock(Client4.getChannelsRoute()). + // get(`/${channelId}/posts`). + // query(true). + // reply(200, postList); + + // const result = await store.dispatch(Actions.getPostsBefore(channelId, 'post3', 0, 10)); + + // expect(result).toEqual({data: postList}); + + // const state = store.getState(); + + // expect(state.entities.posts.posts).toEqual({post1, post2, post3}); + // expect(state.entities.posts.postsInChannel.channel1).toEqual([ + // {order: ['post3', 'post2', 'post1'], recent: false, oldest: true}, + // ]); + // expect(state.entities.posts.postsInThread).toEqual({ + // post1: ['post2'], + // }); + // }); + + // it('getPostsAfter', async () => { + // const channelId = 'channel1'; + + // const post1 = {id: 'post1', channel_id: channelId, create_at: 1001, message: ''}; + // const post2 = {id: 'post2', channel_id: channelId, root_id: 'post1', create_at: 1002, message: ''}; + // const post3 = {id: 'post3', channel_id: channelId, create_at: 1003, message: ''}; + + // store = await configureStore({ + // entities: { + // posts: { + // posts: { + // post1, + // }, + // postsInChannel: { + // channel1: [ + // {order: ['post1'], recent: false}, + // ], + // }, + // }, + // }, + // }); + + // const postList = { + // order: [post3.id, post2.id], + // posts: { + // post2, + // post3, + // }, + // }; + + // nock(Client4.getChannelsRoute()). + // get(`/${channelId}/posts`). + // query(true). + // reply(200, postList); + + // const result = await store.dispatch(Actions.getPostsAfter(channelId, 'post1', 0, 10)); + + // expect(result).toEqual({data: postList}); + + // const state = store.getState(); + + // expect(state.entities.posts.posts).toEqual({post1, post2, post3}); + // expect(state.entities.posts.postsInChannel.channel1).toEqual([ + // {order: ['post3', 'post2', 'post1'], recent: false}, + // ]); + // expect(state.entities.posts.postsInThread).toEqual({ + // post1: ['post2'], + // }); + // }); + + // it('getPostsAfter with empty next_post_id', async () => { + // const channelId = 'channel1'; + + // const post1 = {id: 'post1', channel_id: channelId, create_at: 1001, message: ''}; + // const post2 = {id: 'post2', channel_id: channelId, root_id: 'post1', create_at: 1002, message: ''}; + // const post3 = {id: 'post3', channel_id: channelId, create_at: 1003, message: ''}; + + // store = await configureStore({ + // entities: { + // posts: { + // posts: { + // post1, + // }, + // postsInChannel: { + // channel1: [ + // {order: ['post1'], recent: false}, + // ], + // }, + // }, + // }, + // }); + + // const postList = { + // order: [post3.id, post2.id], + // posts: { + // post2, + // post3, + // }, + // next_post_id: '', + // }; + + // nock(Client4.getChannelsRoute()). + // get(`/${channelId}/posts`). + // query(true). + // reply(200, postList); + + // const result = await store.dispatch(Actions.getPostsAfter(channelId, 'post1', 0, 10)); + + // expect(result).toEqual({data: postList}); + + // const state = store.getState(); + + // expect(state.entities.posts.posts).toEqual({post1, post2, post3}); + // expect(state.entities.posts.postsInChannel.channel1).toEqual([ + // {order: ['post3', 'post2', 'post1'], recent: true}, + // ]); + // }); + + // it('getPostsAround', async () => { + // const postId = 'post3'; + // const channelId = 'channel1'; + + // const postsAfter = { + // posts: { + // post1: {id: 'post1', create_at: 10002, message: ''}, + // post2: {id: 'post2', create_at: 10001, message: ''}, + // }, + // order: ['post1', 'post2'], + // next_post_id: 'post0', + // before_post_id: 'post3', + // }; + // const postsThread = { + // posts: { + // root: {id: 'root', create_at: 10010, message: ''}, + // post3: {id: 'post3', root_id: 'root', create_at: 10000, message: ''}, + // }, + // order: ['post3'], + // next_post_id: 'post2', + // before_post_id: 'post5', + // }; + // const postsBefore = { + // posts: { + // post4: {id: 'post4', create_at: 9999, message: ''}, + // post5: {id: 'post5', create_at: 9998, message: ''}, + // }, + // order: ['post4', 'post5'], + // next_post_id: 'post3', + // before_post_id: 'post6', + // }; + + // nock(Client4.getChannelsRoute()). + // get(`/${channelId}/posts`). + // query((params) => Boolean(params.after)). + // reply(200, postsAfter); + // nock(Client4.getChannelsRoute()). + // get(`/${channelId}/posts`). + // query((params) => Boolean(params.before)). + // reply(200, postsBefore); + // nock(Client4.getBaseRoute()). + // get(`/posts/${postId}/thread`). + // query(true). + // reply(200, postsThread); + + // const result = await store.dispatch(Actions.getPostsAround(channelId, postId)); + + // expect(result.error).toBeFalsy(); + // expect(result.data).toEqual({ + // posts: { + // ...postsAfter.posts, + // ...postsThread.posts, + // ...postsBefore.posts, + // }, + // order: [ + // ...postsAfter.order, + // postId, + // ...postsBefore.order, + // ], + // next_post_id: postsAfter.next_post_id, + // prev_post_id: postsBefore.prev_post_id, + // }); + + // const {posts, postsInChannel, postsInThread} = store.getState().entities.posts; + + // // should store all of the posts + // expect(posts).toHaveProperty('post1'); + // expect(posts).toHaveProperty('post2'); + // expect(posts).toHaveProperty('post3'); + // expect(posts).toHaveProperty('post4'); + // expect(posts).toHaveProperty('post5'); + // expect(posts).toHaveProperty('root'); + + // // should only store the posts that we know the order of + // expect(postsInChannel[channelId]).toEqual([{order: ['post1', 'post2', 'post3', 'post4', 'post5'], recent: false, oldest: false}]); + + // // should populate postsInThread + // expect(postsInThread.root).toEqual(['post3']); + // }); + + // it('flagPost', async () => { + // const {dispatch, getState} = store; + // const channelId = TestHelper.basicChannel.id; + + // nock(Client4.getUsersRoute()). + // post('/logout'). + // reply(200, OK_RESPONSE); + // await TestHelper.basicClient4.logout(); + + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + + // const post1 = await Client4.createPost( + // TestHelper.fakePost(channelId), + // ); + + // nock(Client4.getUsersRoute()). + // put(`/${TestHelper.basicUser.id}/preferences`). + // reply(200, OK_RESPONSE); + + // Actions.flagPost(post1.id)(dispatch, getState); + // const state = getState(); + // const prefKey = getPreferenceKey(Preferences.CATEGORY_FLAGGED_POST, post1.id); + // const preference = state.entities.preferences.myPreferences[prefKey]; + // assert.ok(preference); + // }); + + // it('unflagPost', async () => { + // const {dispatch, getState} = store; + // const channelId = TestHelper.basicChannel.id; + // nock(Client4.getUsersRoute()). + // post('/logout'). + // reply(200, OK_RESPONSE); + // await TestHelper.basicClient4.logout(); + + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(channelId), + // ); + + // nock(Client4.getUsersRoute()). + // put(`/${TestHelper.basicUser.id}/preferences`). + // reply(200, OK_RESPONSE); + // Actions.flagPost(post1.id)(dispatch, getState); + // let state = getState(); + // const prefKey = getPreferenceKey(Preferences.CATEGORY_FLAGGED_POST, post1.id); + // const preference = state.entities.preferences.myPreferences[prefKey]; + // assert.ok(preference); + + // nock(Client4.getUsersRoute()). + // delete(`/${TestHelper.basicUser.id}/preferences`). + // reply(200, OK_RESPONSE); + // Actions.unflagPost(post1.id)(dispatch, getState); + // state = getState(); + // const unflagged = state.entities.preferences.myPreferences[prefKey]; + // assert.ifError(unflagged); + // }); + + // it('setUnreadPost', async () => { + // const teamId = TestHelper.generateId(); + // const channelId = TestHelper.generateId(); + // const userId = TestHelper.generateId(); + // const postId = TestHelper.generateId(); + + // store = await configureStore({ + // entities: { + // channels: { + // channels: { + // [channelId]: {team_id: teamId, total_msg_count: 10}, + // }, + // myMembers: { + // [channelId]: {msg_count: 10, mention_count: 0, last_viewed_at: 0}, + // }, + // }, + // teams: { + // myMembers: { + // [teamId]: {msg_count: 15, mention_count: 0}, + // }, + // }, + // users: { + // currentUserId: userId, + // }, + // posts: { + // posts: { + // [postId]: {id: postId, msg: 'test message', create_at: 123, delete_at: 0, channel_id: channelId}, + // }, + // }, + // }, + // }); + + // nock(Client4.getUserRoute(userId)).post(`/posts/${postId}/set_unread`).reply(200, { + // team_id: teamId, + // channel_id: channelId, + // msg_count: 3, + // last_viewed_at: 1565605543, + // mention_count: 1, + // }); + + // await store.dispatch(Actions.setUnreadPost(userId, postId)); + // const state = store.getState(); + + // assert.equal(state.entities.channels.channels[channelId].total_msg_count, 10); + // assert.equal(state.entities.channels.myMembers[channelId].msg_count, 3); + // assert.equal(state.entities.channels.myMembers[channelId].mention_count, 1); + // assert.equal(state.entities.channels.myMembers[channelId].last_viewed_at, 1565605543); + // assert.equal(state.entities.teams.myMembers[teamId].msg_count, 8); + // assert.equal(state.entities.teams.myMembers[teamId].mention_count, 1); + // }); + + // it('pinPost', async () => { + // const {dispatch, getState} = store; + + // nock(Client4.getBaseRoute()). + // get(`/channels/${TestHelper.basicChannel.id}/stats`). + // reply(200, {channel_id: TestHelper.basicChannel.id, member_count: 1, pinnedpost_count: 0}); + + // await dispatch(getChannelStats(TestHelper.basicChannel.id)); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const postList = {order: [post1.id], posts: {}}; + // postList.posts[post1.id] = post1; + + // nock(Client4.getBaseRoute()). + // get(`/posts/${post1.id}/thread`). + // reply(200, postList); + // await Actions.getPostThread(post1.id)(dispatch, getState); + + // nock(Client4.getBaseRoute()). + // post(`/posts/${post1.id}/pin`). + // reply(200, OK_RESPONSE); + // await Actions.pinPost(post1.id)(dispatch, getState); + + // const state = getState(); + // const {stats} = state.entities.channels; + // const post = state.entities.posts.posts[post1.id]; + // const pinned_post_count = stats[TestHelper.basicChannel.id].pinnedpost_count; + + // assert.ok(post); + // assert.ok(post.is_pinned === true); + // assert.ok(pinned_post_count === 1); + // }); + + // it('unpinPost', async () => { + // const {dispatch, getState} = store; + + // nock(Client4.getBaseRoute()). + // get(`/channels/${TestHelper.basicChannel.id}/stats`). + // reply(200, {channel_id: TestHelper.basicChannel.id, member_count: 1, pinnedpost_count: 0}); + + // await dispatch(getChannelStats(TestHelper.basicChannel.id)); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const postList = {order: [post1.id], posts: {}}; + // postList.posts[post1.id] = post1; + + // nock(Client4.getBaseRoute()). + // get(`/posts/${post1.id}/thread`). + // reply(200, postList); + // await Actions.getPostThread(post1.id)(dispatch, getState); + + // nock(Client4.getBaseRoute()). + // post(`/posts/${post1.id}/pin`). + // reply(200, OK_RESPONSE); + // await Actions.pinPost(post1.id)(dispatch, getState); + + // nock(Client4.getBaseRoute()). + // post(`/posts/${post1.id}/unpin`). + // reply(200, OK_RESPONSE); + // await Actions.unpinPost(post1.id)(dispatch, getState); + + // const state = getState(); + // const {stats} = state.entities.channels; + // const post = state.entities.posts.posts[post1.id]; + // const pinned_post_count = stats[TestHelper.basicChannel.id].pinnedpost_count; + + // assert.ok(post); + // assert.ok(post.is_pinned === false); + // assert.ok(pinned_post_count === 0); + // }); + + // it('addReaction', async () => { + // const {dispatch, getState} = store; + + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(dispatch, getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const emojiName = '+1'; + + // nock(Client4.getBaseRoute()). + // post('/reactions'). + // reply(201, {user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName, create_at: 1508168444721}); + // await Actions.addReaction(post1.id, emojiName)(dispatch, getState); + + // const state = getState(); + // const reactions = state.entities.posts.reactions[post1.id]; + // assert.ok(reactions); + // assert.ok(reactions[TestHelper.basicUser.id + '-' + emojiName]); + // }); + + // it('removeReaction', async () => { + // const {dispatch, getState} = store; + + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(dispatch, getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const emojiName = '+1'; + + // nock(Client4.getBaseRoute()). + // post('/reactions'). + // reply(201, {user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName, create_at: 1508168444721}); + // await Actions.addReaction(post1.id, emojiName)(dispatch, getState); + + // nock(Client4.getUsersRoute()). + // delete(`/${TestHelper.basicUser.id}/posts/${post1.id}/reactions/${emojiName}`). + // reply(200, OK_RESPONSE); + // await Actions.removeReaction(post1.id, emojiName)(dispatch, getState); + + // const state = getState(); + // const reactions = state.entities.posts.reactions[post1.id]; + // assert.ok(reactions); + // assert.ok(!reactions[TestHelper.basicUser.id + '-' + emojiName]); + // }); + + // it('getReactionsForPost', async () => { + // const {dispatch, getState} = store; + + // TestHelper.mockLogin(); + // await login(TestHelper.basicUser.email, 'password1')(dispatch, getState); + + // nock(Client4.getBaseRoute()). + // post('/posts'). + // reply(201, TestHelper.fakePostWithId(TestHelper.basicChannel.id)); + // const post1 = await Client4.createPost( + // TestHelper.fakePost(TestHelper.basicChannel.id), + // ); + + // const emojiName = '+1'; + + // nock(Client4.getBaseRoute()). + // post('/reactions'). + // reply(201, {user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName, create_at: 1508168444721}); + // await Actions.addReaction(post1.id, emojiName)(dispatch, getState); + + // dispatch({ + // type: PostTypes.REACTION_DELETED, + // data: {user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName}, + // }); + + // nock(Client4.getBaseRoute()). + // get(`/posts/${post1.id}/reactions`). + // reply(200, [{user_id: TestHelper.basicUser.id, post_id: post1.id, emoji_name: emojiName, create_at: 1508168444721}]); + // await Actions.getReactionsForPost(post1.id)(dispatch, getState); + + // const state = getState(); + // const reactions = state.entities.posts.reactions[post1.id]; + + // assert.ok(reactions); + // assert.ok(reactions[TestHelper.basicUser.id + '-' + emojiName]); + // }); + + // it('getCustomEmojiForReaction', async () => { + // const testImageData = fs.createReadStream('test/assets/images/test.png'); + // const {dispatch, getState} = store; + + // nock(Client4.getBaseRoute()). + // post('/emoji'). + // reply(201, {id: TestHelper.generateId(), create_at: 1507918415696, update_at: 1507918415696, delete_at: 0, creator_id: TestHelper.basicUser.id, name: TestHelper.generateId()}); + + // const {data: created} = await createCustomEmoji( + // { + // name: TestHelper.generateId(), + // creator_id: TestHelper.basicUser.id, + // }, + // testImageData, + // )(store.dispatch, store.getState); + + // nock(Client4.getEmojisRoute()). + // get(`/name/${created.name}`). + // reply(200, created); + + // const missingEmojiName = ':notrealemoji:'; + + // nock(Client4.getEmojisRoute()). + // get(`/name/${missingEmojiName}`). + // reply(404, {message: 'Not found', status_code: 404}); + + // await Actions.getCustomEmojiForReaction(missingEmojiName)(dispatch, getState); + + // const state = getState(); + // const emojis = state.entities.emojis.customEmoji; + // assert.ok(emojis); + // assert.ok(emojis[created.id]); + // assert.ok(state.entities.emojis.nonExistentEmoji.has(missingEmojiName)); + // }); + + // it('getOpenGraphMetadata', async () => { + // const {dispatch, getState} = store; + + // const url = 'https://about.mattermost.com'; + // const docs = 'https://docs.mattermost.com/'; + + // nock(Client4.getBaseRoute()). + // post('/opengraph'). + // reply(200, {type: 'article', url: 'https://about.mattermost.com/', title: 'Mattermost private cloud messaging', description: 'Open source, private cloud\nSlack-alternative, \nWorkplace messaging for web, PCs and phones.'}); + // await dispatch(Actions.getOpenGraphMetadata(url)); + + // nock(Client4.getBaseRoute()). + // post('/opengraph'). + // reply(200, {type: '', url: '', title: '', description: ''}); + // await dispatch(Actions.getOpenGraphMetadata(docs)); + + // nock(Client4.getBaseRoute()). + // post('/opengraph'). + // reply(200, null); + // await dispatch(Actions.getOpenGraphMetadata(docs)); + + // const state = getState(); + // const metadata = state.entities.posts.openGraph; + // assert.ok(metadata); + // assert.ok(metadata[url]); + // assert.ifError(metadata[docs]); + // }); + + // it('doPostAction', async () => { + // nock(Client4.getBaseRoute()). + // post('/posts/posth67ja7ntdkek6g13dp3wka/actions/action7ja7ntdkek6g13dp3wka'). + // reply(200, {}); + + // const {data} = await Actions.doPostAction('posth67ja7ntdkek6g13dp3wka', 'action7ja7ntdkek6g13dp3wka', 'option')(store.dispatch, store.getState); + // assert.deepEqual(data, {}); + // }); + + // it('doPostActionWithCookie', async () => { + // nock(Client4.getBaseRoute()). + // post('/posts/posth67ja7ntdkek6g13dp3wka/actions/action7ja7ntdkek6g13dp3wka'). + // reply(200, {}); + + // const {data} = await Actions.doPostActionWithCookie('posth67ja7ntdkek6g13dp3wka', 'action7ja7ntdkek6g13dp3wka', '', 'option')(store.dispatch, store.getState); + // assert.deepEqual(data, {}); + // }); + + // it('addMessageIntoHistory', async () => { + // const {dispatch, getState} = store; + + // await Actions.addMessageIntoHistory('test1')(dispatch, getState); + + // let history = getState().entities.posts.messagesHistory.messages; + // assert.ok(history.length === 1); + // assert.ok(history[0] === 'test1'); + + // await Actions.addMessageIntoHistory('test2')(dispatch, getState); + + // history = getState().entities.posts.messagesHistory.messages; + // assert.ok(history.length === 2); + // assert.ok(history[1] === 'test2'); + + // await Actions.addMessageIntoHistory('test3')(dispatch, getState); + + // history = getState().entities.posts.messagesHistory.messages; + // assert.ok(history.length === 3); + // assert.ok(history[2] === 'test3'); + // }); + + // it('resetHistoryIndex', async () => { + // const {dispatch, getState} = store; + + // await Actions.addMessageIntoHistory('test1')(dispatch, getState); + // await Actions.addMessageIntoHistory('test2')(dispatch, getState); + // await Actions.addMessageIntoHistory('test3')(dispatch, getState); + + // let index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 3); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.COMMENT)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 1); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 2); + + // await Actions.resetHistoryIndex(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 3); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 2); + + // await Actions.resetHistoryIndex(Posts.MESSAGE_TYPES.COMMENT)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 3); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + // }); + + // it('moveHistoryIndexBack', async () => { + // const {dispatch, getState} = store; + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // let index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === -1); + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === -1); + + // await Actions.addMessageIntoHistory('test1')(dispatch, getState); + // await Actions.addMessageIntoHistory('test2')(dispatch, getState); + // await Actions.addMessageIntoHistory('test3')(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 3); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 1); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 0); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.COMMENT)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 0); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 2); + // }); + + // it('moveHistoryIndexForward', async () => { + // const {dispatch, getState} = store; + + // await Actions.moveHistoryIndexForward(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // let index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 0); + + // await Actions.moveHistoryIndexForward(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 0); + + // await Actions.addMessageIntoHistory('test1')(dispatch, getState); + // await Actions.addMessageIntoHistory('test2')(dispatch, getState); + // await Actions.addMessageIntoHistory('test3')(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 3); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + + // await Actions.moveHistoryIndexForward(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexForward(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 3); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 3); + + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.COMMENT)(dispatch, getState); + // await Actions.moveHistoryIndexBack(Posts.MESSAGE_TYPES.COMMENT)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 1); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 1); + + // await Actions.moveHistoryIndexForward(Posts.MESSAGE_TYPES.POST)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 2); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 1); + + // await Actions.moveHistoryIndexForward(Posts.MESSAGE_TYPES.COMMENT)(dispatch, getState); + + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.POST]; + // assert.ok(index === 2); + // index = getState().entities.posts.messagesHistory.index[Posts.MESSAGE_TYPES.COMMENT]; + // assert.ok(index === 2); + // }); + + // describe('getProfilesAndStatusesForPosts', () => { + // describe('different values for posts argument', () => { + // // Mock the state to prevent any followup requests since we aren't testing those + // const currentUserId = 'user'; + // const post = {id: 'post', user_id: currentUserId, message: 'This is a post'}; + + // const dispatch = null; + // const getState = () => ({ + // entities: { + // general: { + // config: { + // EnableCustomEmoji: 'false', + // }, + // }, + // users: { + // currentUserId, + // statuses: { + // [currentUserId]: 'status', + // }, + // }, + // }, + // }); + + // it('null', async () => { + // await Actions.getProfilesAndStatusesForPosts(null, dispatch, getState); + // }); + + // it('array of posts', async () => { + // const posts = [post]; + + // await Actions.getProfilesAndStatusesForPosts(posts, dispatch, getState); + // }); + + // it('object map of posts', async () => { + // const posts = { + // [post.id]: post, + // }; + + // await Actions.getProfilesAndStatusesForPosts(posts, dispatch, getState); + // }); + // }); + // }); + + // describe('getThreadsForPosts', () => { + // beforeAll(async () => { + // await TestHelper.initBasic(Client4); + // }); + + // afterAll(async () => { + // await TestHelper.tearDown(); + // }); + + // let channelId; + // let post1; + // let post2; + // let post3; + // let comment; + + // beforeEach(async () => { + // store = await configureStore(); + + // channelId = TestHelper.basicChannel.id; + // post1 = {id: TestHelper.generateId(), channel_id: channelId, message: ''}; + // post2 = {id: TestHelper.generateId(), channel_id: channelId, message: ''}; + // comment = {id: TestHelper.generateId(), root_id: post1.id, channel_id: channelId, message: ''}; + // post3 = {id: TestHelper.generateId(), channel_id: channelId, message: ''}; + + // store.dispatch(Actions.receivedPostsInChannel({ + // order: [post2.id, post3.id], + // posts: {[post2.id]: post2, [post3.id]: post3}, + // }, channelId)); + + // const threadList = { + // order: [post1.id], + // posts: { + // [post1.id]: post1, + // [comment.id]: comment, + // }, + // }; + + // nock(Client4.getBaseRoute()). + // get(`/posts/${post1.id}/thread`). + // reply(200, threadList); + // }); + + // it('handlesNull', async () => { + // const ret = await store.dispatch(Actions.getThreadsForPosts(null)); + // expect(ret).toEqual({data: true}); + + // const state = store.getState(); + + // const getRequest = state.requests.posts.getPostThread; + // if (getRequest.status === RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(getRequest.error)); + // } + + // const { + // postsInChannel, + // postsInThread, + // } = state.entities.posts; + + // assert.ok(postsInChannel[channelId]); + // assert.deepEqual(postsInChannel[channelId][0].order, [post2.id, post3.id]); + // assert.ok(!postsInThread[post1.id]); + + // const found = postsInChannel[channelId].find((block) => block.order.indexOf(comment.id) !== -1); + // assert.ok(!found, 'should not have found comment in postsInChannel'); + // }); + + // it('pullsUpTheThreadOfAMissingPost', async () => { + // await store.dispatch(Actions.getThreadsForPosts([comment])); + + // const state = store.getState(); + + // const getRequest = state.requests.posts.getPostThread; + // if (getRequest.status === RequestStatus.FAILURE) { + // throw new Error(JSON.stringify(getRequest.error)); + // } + + // const { + // posts, + // postsInChannel, + // postsInThread, + // } = state.entities.posts; + + // assert.ok(posts); + // assert.deepEqual(postsInChannel[channelId][0].order, [post2.id, post3.id]); + // assert.ok(posts[post1.id]); + // assert.ok(postsInThread[post1.id]); + // assert.deepEqual(postsInThread[post1.id], [comment.id]); + + // const found = postsInChannel[channelId].find((block) => block.order.indexOf(comment.id) !== -1); + // assert.ok(!found, 'should not have found comment in postsInChannel'); + // }); + // }); + + // describe('receivedPostsBefore', () => { + // it('Should return default false for oldest key if param does not exist', () => { + // const posts = []; + // const result = Actions.receivedPostsBefore(posts, 'channelId', 'beforePostId'); + // assert.deepEqual(result, { + // type: PostTypes.RECEIVED_POSTS_BEFORE, + // channelId: 'channelId', + // data: posts, + // beforePostId: 'beforePostId', + // oldest: false, + // }); + // }); + + // it('Should return true for oldest key', () => { + // const posts = []; + // const result = Actions.receivedPostsBefore(posts, 'channelId', 'beforePostId', true); + // assert.deepEqual(result, { + // type: PostTypes.RECEIVED_POSTS_BEFORE, + // channelId: 'channelId', + // data: posts, + // beforePostId: 'beforePostId', + // oldest: true, + // }); + // }); + // }); + + // describe('receivedPostsInChannel', () => { + // it('Should return default false for both recent and oldest keys if params dont exist', () => { + // const posts = []; + // const result = Actions.receivedPostsInChannel(posts, 'channelId'); + // assert.deepEqual(result, { + // type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + // channelId: 'channelId', + // data: posts, + // recent: false, + // oldest: false, + // }); + // }); + + // it('Should return true for oldest and recent keys', () => { + // const posts = []; + // const result = Actions.receivedPostsInChannel(posts, 'channelId', true, true); + // assert.deepEqual(result, { + // type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + // channelId: 'channelId', + // data: posts, + // recent: true, + // oldest: true, + // }); + // }); + // }); + + // describe('lastPostActions', () => { + // test('should mark channel as read when viewing channel', async () => { + // const channelId = TestHelper.generateId(); + + // store = await configureStore({ + // entities: { + // channels: { + // currentChannelId: channelId, + // myMembers: { + // [channelId]: {channel_id: channelId, last_viewed_at: 0, roles: ''}, + // }, + // }, + // }, + // }); + + // const post1 = {channel_id: channelId, create_at: 1000}; + // await store.dispatch(Actions.receivedPost(post1)); + + // const post2 = {channel_id: channelId, create_at: 2000}; + // await store.dispatch(Actions.lastPostActions(post2, {})); + + // expect(store.getState().entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(post2.create_at); + // }); + + // test('should not mark channel as read when not viewing channel', async () => { + // const channelId = TestHelper.generateId(); + // const otherChannelId = TestHelper.generateId(); + + // store = await configureStore({ + // entities: { + // channels: { + // currentChannelId: otherChannelId, + // myMembers: { + // [channelId]: {channel_id: channelId, last_viewed_at: 500, roles: ''}, + // [otherChannelId]: {channel_id: otherChannelId, last_viewed_at: 500, roles: ''}, + // }, + // }, + // }, + // }); + + // const post1 = {channel_id: channelId, create_at: 1000}; + // await store.dispatch(Actions.receivedPost(post1)); + + // const post2 = {channel_id: channelId, create_at: 2000}; + // await store.dispatch(Actions.lastPostActions(post2, {})); + + // expect(store.getState().entities.channels.myMembers[channelId].last_viewed_at).toBe(500); + // }); + + // test('should mark channel as read when not viewing channel and post is from current user', async () => { + // const channelId = TestHelper.generateId(); + // const otherChannelId = TestHelper.generateId(); + // const currentUserId = TestHelper.generateId(); + + // store = await configureStore({ + // entities: { + // channels: { + // currentChannelId: otherChannelId, + // myMembers: { + // [channelId]: {channel_id: channelId, last_viewed_at: 500, roles: ''}, + // [otherChannelId]: {channel_id: otherChannelId, last_viewed_at: 500, roles: ''}, + // }, + // }, + // users: { + // currentUserId, + // }, + // }, + // }); + + // const post1 = {channel_id: channelId, create_at: 1000}; + // await store.dispatch(Actions.receivedPost(post1)); + + // const post2 = {channel_id: channelId, create_at: 2000, user_id: currentUserId}; + // await store.dispatch(Actions.lastPostActions(post2, {})); + + // expect(store.getState().entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(post2.create_at); + // }); + + // test('should mark channel as unread when not viewing channel and post is from webhook owned by current user', async () => { + // const channelId = TestHelper.generateId(); + // const otherChannelId = TestHelper.generateId(); + // const currentUserId = TestHelper.generateId(); + + // store = await configureStore({ + // entities: { + // channels: { + // currentChannelId: otherChannelId, + // myMembers: { + // [channelId]: {channel_id: channelId, last_viewed_at: 500, roles: ''}, + // [otherChannelId]: {channel_id: otherChannelId, last_viewed_at: 500, roles: ''}, + // }, + // }, + // users: { + // currentUserId, + // }, + // }, + // }); + + // const post1 = {channel_id: channelId, create_at: 1000}; + // await store.dispatch(Actions.receivedPost(post1)); + + // const post2 = {channel_id: channelId, create_at: 2000, props: {from_webhook: 'true'}, user_id: currentUserId}; + // await store.dispatch(Actions.lastPostActions(post2, {})); + + // expect(store.getState().entities.channels.myMembers[channelId].last_viewed_at).toBe(500); + // }); + + // test('should not mark channel as read when viewing channel that was marked as unread', async () => { + // const channelId = TestHelper.generateId(); + + // store = await configureStore({ + // entities: { + // channels: { + // currentChannelId: channelId, + // myMembers: { + // [channelId]: {channel_id: channelId, last_viewed_at: 500, roles: ''}, + // }, + // manuallyUnread: { + // [channelId]: true, + // }, + // }, + // }, + // }); + + // const post1 = {id: 'post1', channel_id: channelId, create_at: 1000}; + // await store.dispatch(Actions.receivedPost(post1)); + + // await store.dispatch({ + // type: ChannelTypes.POST_UNREAD_SUCCESS, + // data: { + // channelId, + // lastViewedAt: post1.create_at - 1, + // }, + // }); + + // const post2 = {channel_id: channelId, create_at: 2000}; + // await store.dispatch(Actions.lastPostActions(post2, {})); + + // expect(store.getState().entities.channels.myMembers[channelId].last_viewed_at).toBe(post1.create_at - 1); + // }); + // }); +}); diff --git a/app/mm-redux/actions/posts.ts b/app/mm-redux/actions/posts.ts new file mode 100644 index 000000000..32fbb4b5c --- /dev/null +++ b/app/mm-redux/actions/posts.ts @@ -0,0 +1,1305 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE} from '@mm-redux/client'; +import {General, Preferences, Posts, WebsocketEvents} from '../constants'; +import {PostTypes, ChannelTypes, FileTypes, IntegrationTypes} from '@mm-redux/action_types'; + +import {getCurrentChannelId, getMyChannelMember as getMyChannelMemberSelector, isManuallyUnread} from '@mm-redux/selectors/entities/channels'; +import {getCustomEmojisByName as selectCustomEmojisByName} from '@mm-redux/selectors/entities/emojis'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import * as Selectors from '@mm-redux/selectors/entities/posts'; +import {getCurrentUserId, getUsersByUsername} from '@mm-redux/selectors/entities/users'; + +import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils'; +import {parseNeededCustomEmojisFromText} from '@mm-redux/utils/emoji_utils'; +import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@mm-redux/utils/post_utils'; +import {isCombinedUserActivityPost} from '@mm-redux/utils/post_list'; + +import {getMyChannelMember, markChannelAsUnread, markChannelAsRead, markChannelAsViewed} from './channels'; +import {systemEmojis, getCustomEmojiByName, getCustomEmojisByName} from './emojis'; +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; + +import { + deletePreferences, + makeDirectChannelVisibleIfNecessary, + makeGroupMessageVisibleIfNecessary, + savePreferences, +} from './preferences'; +import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from './users'; +import {Action, ActionResult, batchActions, DispatchFunc, GetStateFunc, GenericAction} from '@mm-redux/types/actions'; +import {ChannelUnread} from '@mm-redux/types/channels'; +import {GlobalState} from '@mm-redux/types/store'; +import {Post} from '@mm-redux/types/posts'; +import {Error} from '@mm-redux/types/errors'; +import {Reaction} from '@mm-redux/types/reactions'; +import {UserProfile} from '@mm-redux/types/users'; +import {Dictionary} from '@mm-redux/types/utilities'; +import {CustomEmoji} from '@mm-redux/types/emojis'; + +// receivedPost should be dispatched after a single post from the server. This typically happens when an existing post +// is updated. +export function receivedPost(post: Post) { + return { + type: PostTypes.RECEIVED_POST, + data: post, + }; +} + +// receivedNewPost should be dispatched when receiving a newly created post or when sending a request to the server +// to make a new post. +export function receivedNewPost(post: Post) { + return { + type: PostTypes.RECEIVED_NEW_POST, + data: post, + }; +} + +// receivedPosts should be dispatched when receiving multiple posts from the server that may or may not be ordered. +// This will typically be used alongside other actions like receivedPostsAfter which require the posts to be ordered. +export function receivedPosts(posts: CombinedPostList) { + return { + type: PostTypes.RECEIVED_POSTS, + data: posts, + }; +} + +// receivedPostsAfter should be dispatched when receiving an ordered list of posts that come before a given post. +export function receivedPostsAfter(posts: Array, channelId: string, afterPostId: string, recent = false) { + return { + type: PostTypes.RECEIVED_POSTS_AFTER, + channelId, + data: posts, + afterPostId, + recent, + }; +} + +// receivedPostsBefore should be dispatched when receiving an ordered list of posts that come after a given post. +export function receivedPostsBefore(posts: Array, channelId: string, beforePostId: string, oldest = false) { + return { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId, + data: posts, + beforePostId, + oldest, + }; +} + +// receivedPostsSince should be dispatched when receiving a list of posts that have been updated since a certain time. +// Due to how the API endpoint works, some of these posts will be ordered, but others will not, so this needs special +// handling from the reducers. +export function receivedPostsSince(posts: Array, channelId: string) { + return { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId, + data: posts, + }; +} + +// receivedPostsInChannel should be dispatched when receiving a list of ordered posts within a channel when the +// the adjacent posts are not known. +export function receivedPostsInChannel(posts: CombinedPostList, channelId: string, recent = false, oldest = false) { + return { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId, + data: posts, + recent, + oldest, + }; +} + +// receivedPostsInThread should be dispatched when receiving a list of unordered posts in a thread. +export function receivedPostsInThread(posts: Array, rootId: string) { + return { + type: PostTypes.RECEIVED_POSTS_IN_THREAD, + data: posts, + rootId, + }; +} + +// postDeleted should be dispatched when a post has been deleted and should be replaced with a "message deleted" +// placeholder. This typically happens when a post is deleted by another user. +export function postDeleted(post: Post) { + return { + type: PostTypes.POST_DELETED, + data: post, + }; +} + +// postRemoved should be dispatched when a post should be immediately removed. This typically happens when a post is +// deleted by the current user. +export function postRemoved(post: Post) { + return { + type: PostTypes.POST_REMOVED, + data: post, + }; +} + +export function getPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let post; + + try { + post = await Client4.getPost(postId); + getProfilesAndStatusesForPosts([post], dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: PostTypes.GET_POSTS_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + receivedPost(post), + { + type: PostTypes.GET_POSTS_SUCCESS, + }, + ])); + + return {data: post}; + }; +} + +export function createPost(post: Post, files: any[] = []) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const currentUserId = state.entities.users.currentUserId; + + const timestamp = Date.now(); + const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`; + + if (Selectors.isPostIdSending(state, pendingPostId)) { + return {data: true}; + } + + let newPost = { + ...post, + pending_post_id: pendingPostId, + create_at: timestamp, + update_at: timestamp, + }; + + // We are retrying a pending post that had files + if (newPost.file_ids && !files.length) { + files = newPost.file_ids.map((id) => state.entities.files.files[id]); // eslint-disable-line + } + + if (files.length) { + const fileIds = files.map((file) => file.id); + + newPost = { + ...newPost, + file_ids: fileIds, + }; + + dispatch({ + type: FileTypes.RECEIVED_FILES_FOR_POST, + postId: pendingPostId, + data: files, + }); + } + + dispatch({ + type: PostTypes.RECEIVED_NEW_POST, + data: { + id: pendingPostId, + ...newPost, + }, + meta: { + offline: { + effect: () => Client4.createPost({...newPost, create_at: 0}), + commit: (result: any, payload: any) => { + const actions: Action[] = [ + receivedPost(payload), + { + type: PostTypes.CREATE_POST_SUCCESS, + }, + { + type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT, + data: { + channelId: newPost.channel_id, + amount: 1, + }, + }, + { + type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT, + data: { + channelId: newPost.channel_id, + amount: 1, + }, + }, + ]; + + if (files) { + actions.push({ + type: FileTypes.RECEIVED_FILES_FOR_POST, + postId: payload.id, + data: files, + }); + } + + dispatch(batchActions(actions)); + }, + maxRetry: 0, + offlineRollback: true, + rollback: (result: any, error: Error) => { + const data = { + ...newPost, + id: pendingPostId, + failed: true, + update_at: Date.now(), + }; + dispatch({type: PostTypes.CREATE_POST_FAILURE, error}); + + // If the failure was because: the root post was deleted or + // TownSquareIsReadOnly=true then remove the post + if (error.server_error_id === 'api.post.create_post.root_id.app_error' || + error.server_error_id === 'api.post.create_post.town_square_read_only' || + error.server_error_id === 'plugin.message_will_be_posted.dismiss_post' + ) { + dispatch(removePost(data) as any); + } else { + dispatch(receivedPost(data)); + } + }, + }, + }, + }); + + return {data: true}; + }; +} + +export function createPostImmediately(post: Post, files: any[] = []) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const currentUserId = state.entities.users.currentUserId; + + const timestamp = Date.now(); + const pendingPostId = `${currentUserId}:${timestamp}`; + + let newPost: Post = { + ...post, + pending_post_id: pendingPostId, + create_at: timestamp, + update_at: timestamp, + }; + + if (files.length) { + const fileIds = files.map((file) => file.id); + + newPost = { + ...newPost, + file_ids: fileIds, + }; + + dispatch({ + type: FileTypes.RECEIVED_FILES_FOR_POST, + postId: pendingPostId, + data: files, + }); + } + + dispatch(receivedNewPost({ + id: pendingPostId, + ...newPost, + })); + + try { + const created = await Client4.createPost({...newPost, create_at: 0}); + newPost.id = created.id; + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: PostTypes.CREATE_POST_FAILURE, error}, + removePost({id: pendingPostId, ...newPost}) as any, + logError(error), + ])); + return {error}; + } + + const actions: Action[] = [ + receivedPost(newPost), + { + type: PostTypes.CREATE_POST_SUCCESS, + }, + { + type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT, + data: { + channelId: newPost.channel_id, + amount: 1, + }, + }, + { + type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT, + data: { + channelId: newPost.channel_id, + amount: 1, + }, + }, + ]; + + if (files) { + actions.push({ + type: FileTypes.RECEIVED_FILES_FOR_POST, + postId: newPost.id, + data: files, + }); + } + + dispatch(batchActions(actions)); + + return {data: newPost}; + }; +} + +export function resetCreatePostRequest() { + return {type: PostTypes.CREATE_POST_RESET_REQUEST}; +} + +export function deletePost(post: ExtendedPost) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const delPost = {...post}; + if (delPost.type === Posts.POST_TYPES.COMBINED_USER_ACTIVITY && delPost.system_post_ids) { + delPost.system_post_ids.forEach((systemPostId) => { + const systemPost = Selectors.getPost(state, systemPostId); + if (systemPost) { + dispatch(deletePost(systemPost)); + } + }); + } else { + dispatch({ + type: PostTypes.POST_DELETED, + data: delPost, + meta: { + offline: { + effect: () => Client4.deletePost(post.id), + commit: {type: 'do_nothing'}, // redux-offline always needs to dispatch something on commit + rollback: receivedPost(delPost), + }, + }, + }); + } + + return {data: true}; + }; +} + +export function editPost(post: Post) { + return bindClientFunc({ + clientFunc: Client4.patchPost, + onRequest: PostTypes.EDIT_POST_REQUEST, + onSuccess: [PostTypes.RECEIVED_POST, PostTypes.EDIT_POST_SUCCESS], + onFailure: PostTypes.EDIT_POST_FAILURE, + params: [ + post, + ], + }); +} + +export function getUnreadPostData(unreadChan: ChannelUnread, state: GlobalState) { + const member = getMyChannelMemberSelector(state, unreadChan.channel_id); + const delta = member ? member.msg_count - unreadChan.msg_count : unreadChan.msg_count; + + const data = { + teamId: unreadChan.team_id, + channelId: unreadChan.channel_id, + msgCount: unreadChan.msg_count, + mentionCount: unreadChan.mention_count, + lastViewedAt: unreadChan.last_viewed_at, + deltaMsgs: delta, + }; + + return data; +} + +export function setUnreadPost(userId: string, postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let state = getState(); + const post = Selectors.getPost(state, postId); + let unreadChan; + + try { + if (isCombinedUserActivityPost(postId)) { + return {}; + } + unreadChan = await Client4.markPostAsUnread(userId, postId); + dispatch({ + type: ChannelTypes.ADD_MANUALLY_UNREAD, + data: { + channelId: post.channel_id, + }, + }); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + dispatch({ + type: ChannelTypes.REMOVE_MANUALLY_UNREAD, + data: { + channelId: post.channel_id, + }, + }); + return {error}; + } + + state = getState(); + const data = getUnreadPostData(unreadChan, state); + dispatch({ + type: ChannelTypes.POST_UNREAD_SUCCESS, + data, + }); + return {data}; + }; +} + +export function pinPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: PostTypes.EDIT_POST_REQUEST}); + let posts; + + try { + posts = await Client4.pinPost(postId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: PostTypes.EDIT_POST_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + const actions: Action[] = [ + { + type: PostTypes.EDIT_POST_SUCCESS, + }, + ]; + + const post = Selectors.getPost(getState(), postId); + if (post) { + actions.push( + receivedPost({ + ...post, + is_pinned: true, + update_at: Date.now(), + }), + { + type: ChannelTypes.INCREMENT_PINNED_POST_COUNT, + id: post.channel_id, + } + ); + } + + dispatch(batchActions(actions)); + + return {data: posts}; + }; +} + +export function unpinPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: PostTypes.EDIT_POST_REQUEST}); + let posts; + + try { + posts = await Client4.unpinPost(postId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: PostTypes.EDIT_POST_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + const actions: Action[] = [ + { + type: PostTypes.EDIT_POST_SUCCESS, + }, + ]; + + const post = Selectors.getPost(getState(), postId); + if (post) { + actions.push( + receivedPost({ + ...post, + is_pinned: false, + update_at: Date.now(), + }), + { + type: ChannelTypes.DECREMENT_PINNED_POST_COUNT, + id: post.channel_id, + }, + ); + } + + dispatch(batchActions(actions)); + + return {data: posts}; + }; +} + +export function addReaction(postId: string, emojiName: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const currentUserId = getState().entities.users.currentUserId; + + let reaction; + try { + reaction = await Client4.addReaction(currentUserId, postId, emojiName); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: PostTypes.RECEIVED_REACTION, + data: reaction, + }); + + return {data: true}; + }; +} + +export function removeReaction(postId: string, emojiName: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const currentUserId = getState().entities.users.currentUserId; + + try { + await Client4.removeReaction(currentUserId, postId, emojiName); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: PostTypes.REACTION_DELETED, + data: {user_id: currentUserId, post_id: postId, emoji_name: emojiName}, + }); + + return {data: true}; + }; +} + +export function getCustomEmojiForReaction(name: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const nonExistentEmoji = getState().entities.emojis.nonExistentEmoji; + const customEmojisByName = selectCustomEmojisByName(getState()); + + if (systemEmojis.has(name)) { + return {data: true}; + } + + if (nonExistentEmoji.has(name)) { + return {data: true}; + } + + if (customEmojisByName.has(name)) { + return {data: true}; + } + + return dispatch(getCustomEmojiByName(name)); + }; +} + +export function getReactionsForPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let reactions; + try { + reactions = await Client4.getReactionsForPost(postId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + if (reactions && reactions.length > 0) { + const nonExistentEmoji = getState().entities.emojis.nonExistentEmoji; + const customEmojisByName = selectCustomEmojisByName(getState()); + const emojisToLoad = new Set(); + + reactions.forEach((r: Reaction) => { + const name = r.emoji_name; + + if (systemEmojis.has(name)) { + // It's a system emoji, go the next match + return; + } + + if (nonExistentEmoji.has(name)) { + // We've previously confirmed this is not a custom emoji + return; + } + + if (customEmojisByName.has(name)) { + // We have the emoji, go to the next match + return; + } + + emojisToLoad.add(name); + }); + + dispatch(getCustomEmojisByName(Array.from(emojisToLoad))); + } + + dispatch(batchActions([ + { + type: PostTypes.RECEIVED_REACTIONS, + data: reactions, + postId, + }, + ])); + + return reactions; + }; +} + +export function flagPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + const preference = { + user_id: currentUserId, + category: Preferences.CATEGORY_FLAGGED_POST, + name: postId, + value: 'true', + }; + + Client4.trackEvent('action', 'action_posts_flag'); + + return savePreferences(currentUserId, [preference])(dispatch); + }; +} + +export function getPostThread(rootId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: PostTypes.GET_POST_THREAD_REQUEST}); + + let posts; + try { + posts = await Client4.getPostThread(rootId); + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: PostTypes.GET_POST_THREAD_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsInThread(posts, rootId), + { + type: PostTypes.GET_POST_THREAD_SUCCESS, + }, + ])); + + return {data: posts}; + }; +} + +export function getPosts(channelId: string, page = 0, perPage = Posts.POST_CHUNK_SIZE) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let posts; + + try { + posts = await Client4.getPosts(channelId, page, perPage); + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsInChannel(posts, channelId, page === 0, posts.prev_post_id === ''), + ])); + + return {data: posts}; + }; +} + +export function getPostsUnread(channelId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const userId = getCurrentUserId(getState()); + let posts; + try { + posts = await Client4.getPostsUnread(channelId, userId); + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''), + ])); + dispatch({ + type: PostTypes.RECEIVED_POSTS, + data: posts, + channelId, + }); + + return {data: posts}; + }; +} + +export function getPostsSince(channelId: string, since: number) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let posts; + try { + posts = await Client4.getPostsSince(channelId, since); + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsSince(posts, channelId), + { + type: PostTypes.GET_POSTS_SINCE_SUCCESS, + }, + ])); + + return {data: posts}; + }; +} + +export function getPostsBefore(channelId: string, postId: string, page = 0, perPage = Posts.POST_CHUNK_SIZE) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let posts; + try { + posts = await Client4.getPostsBefore(channelId, postId, page, perPage); + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsBefore(posts, channelId, postId, posts.prev_post_id === ''), + ])); + + return {data: posts}; + }; +} + +export function getPostsAfter(channelId: string, postId: string, page = 0, perPage = Posts.POST_CHUNK_SIZE) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let posts; + try { + posts = await Client4.getPostsAfter(channelId, postId, page, perPage); + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsAfter(posts, channelId, postId, posts.next_post_id === ''), + ])); + + return {data: posts}; + }; +} +export type CombinedPostList = { + posts: Array; + order: Array; + next_post_id: string; + prev_post_id: string; +} + +export function getPostsAround(channelId: string, postId: string, perPage = Posts.POST_CHUNK_SIZE / 2) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let after; + let thread; + let before; + + try { + [after, thread, before] = await Promise.all([ + Client4.getPostsAfter(channelId, postId, 0, perPage), + Client4.getPostThread(postId), + Client4.getPostsBefore(channelId, postId, 0, perPage), + ]); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + // Dispatch a combined post list so that the order is correct for postsInChannel + const posts: CombinedPostList = { + posts: { + ...after.posts, + ...thread.posts, + ...before.posts, + }, + order: [ // Remember that the order is newest posts first + ...after.order, + postId, + ...before.order, + ], + next_post_id: after.next_post_id, + prev_post_id: before.prev_post_id, + }; + + getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + + dispatch(batchActions([ + receivedPosts(posts), + receivedPostsInChannel(posts, channelId, after.next_post_id === '', before.prev_post_id === ''), + ])); + + return {data: posts}; + }; +} + +// getThreadsForPosts is intended for an array of posts that have been batched +// (see the actions/websocket_actions/handleNewPostEvents function in the webapp) +export function getThreadsForPosts(posts: Array) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (!Array.isArray(posts) || !posts.length) { + return {data: true}; + } + + const state = getState(); + const promises: Promise[] = []; + + posts.forEach((post) => { + if (!post.root_id) { + return; + } + + const rootPost = Selectors.getPost(state, post.root_id); + if (!rootPost) { + promises.push(dispatch(getPostThread(post.root_id))); + } + }); + + return Promise.all(promises); + }; +} + +// Note that getProfilesAndStatusesForPosts can take either an array of posts or a map of ids to posts +export function getProfilesAndStatusesForPosts(postsArrayOrMap: Array|Map, dispatch: DispatchFunc, getState: GetStateFunc) { + if (!postsArrayOrMap) { + // Some API methods return {error} for no results + return Promise.resolve(); + } + + const posts = Object.values(postsArrayOrMap); + + if (posts.length === 0) { + return Promise.resolve(); + } + + const state = getState(); + const {currentUserId, profiles, statuses} = state.entities.users; + + // Statuses and profiles of the users who made the posts + const userIdsToLoad = new Set(); + const statusesToLoad = new Set(); + + Object.values(posts).forEach((post) => { + const userId = post.user_id; + + if (!statuses[userId]) { + statusesToLoad.add(userId); + } + + if (userId === currentUserId) { + return; + } + + if (!profiles[userId]) { + userIdsToLoad.add(userId); + } + }); + + const promises: any[] = []; + if (userIdsToLoad.size > 0) { + promises.push(getProfilesByIds(Array.from(userIdsToLoad))(dispatch, getState)); + } + + if (statusesToLoad.size > 0) { + promises.push(getStatusesByIds(Array.from(statusesToLoad))(dispatch, getState)); + } + + // Profiles of users mentioned in the posts + const usernamesToLoad = getNeededAtMentionedUsernames(state, posts); + + if (usernamesToLoad.size > 0) { + promises.push(getProfilesByUsernames(Array.from(usernamesToLoad))(dispatch, getState)); + } + + // Emojis used in the posts + const emojisToLoad = getNeededCustomEmojis(state, posts); + + if (emojisToLoad && emojisToLoad.size > 0) { + promises.push(getCustomEmojisByName(Array.from(emojisToLoad))(dispatch, getState)); + } + + return Promise.all(promises); +} + +export function getNeededAtMentionedUsernames(state: GlobalState, posts: Array): Set { + let usersByUsername: Dictionary; // Populate this lazily since it's relatively expensive + + const usernamesToLoad = new Set(); + + posts.forEach((post) => { + if (!post.message.includes('@')) { + return; + } + + if (!usersByUsername) { + usersByUsername = getUsersByUsername(state); + } + + const pattern = /\B@(([a-z0-9_.-]*[a-z0-9_])[.-]*)/gi; + + let match; + while ((match = pattern.exec(post.message)) !== null) { + // match[1] is the matched mention including trailing punctuation + // match[2] is the matched mention without trailing punctuation + if (General.SPECIAL_MENTIONS.indexOf(match[2]) !== -1) { + continue; + } + + if (usersByUsername[match[1]] || usersByUsername[match[2]]) { + // We have the user, go to the next match + continue; + } + + // If there's no trailing punctuation, this will only add 1 item to the set + usernamesToLoad.add(match[1]); + usernamesToLoad.add(match[2]); + } + }); + + return usernamesToLoad; +} + +function buildPostAttachmentText(attachments: Array) { + let attachmentText = ''; + + attachments.forEach((a) => { + if (a.fields && a.fields.length) { + a.fields.forEach((f: any) => { + attachmentText += ' ' + (f.value || ''); + }); + } + + if (a.pretext) { + attachmentText += ' ' + a.pretext; + } + + if (a.text) { + attachmentText += ' ' + a.text; + } + }); + + return attachmentText; +} + +export function getNeededCustomEmojis(state: GlobalState, posts: Array): Set { + if (getConfig(state).EnableCustomEmoji !== 'true') { + return new Set(); + } + + // If post metadata is supported, custom emojis will have been provided as part of that + if (posts[0].metadata) { + return new Set(); + } + + let customEmojisToLoad = new Set(); + + let customEmojisByName: Map; // Populate this lazily since it's relatively expensive + const nonExistentEmoji = state.entities.emojis.nonExistentEmoji; + + posts.forEach((post) => { + if (post.message.includes(':')) { + if (!customEmojisByName) { + customEmojisByName = selectCustomEmojisByName(state); + } + + const emojisFromPost = parseNeededCustomEmojisFromText(post.message, systemEmojis, customEmojisByName, nonExistentEmoji); + + if (emojisFromPost.size > 0) { + customEmojisToLoad = new Set([...customEmojisToLoad, ...emojisFromPost]); + } + } + + const props = post.props; + if (props && props.attachments && props.attachments.length) { + if (!customEmojisByName) { + customEmojisByName = selectCustomEmojisByName(state); + } + + const attachmentText = buildPostAttachmentText(props.attachments); + + if (attachmentText) { + const emojisFromAttachment = parseNeededCustomEmojisFromText(attachmentText, systemEmojis, customEmojisByName, nonExistentEmoji); + + if (emojisFromAttachment.size > 0) { + customEmojisToLoad = new Set([...customEmojisToLoad, ...emojisFromAttachment]); + } + } + } + }); + + return customEmojisToLoad; +} +export type ExtendedPost = Post & { system_post_ids?: string[] }; + +export function removePost(post: ExtendedPost) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (post.type === Posts.POST_TYPES.COMBINED_USER_ACTIVITY && post.system_post_ids) { + const state = getState(); + + for (const systemPostId of post.system_post_ids) { + const systemPost = Selectors.getPost(state, systemPostId); + + if (systemPost) { + dispatch(removePost(systemPost as any) as any); + } + } + } else { + dispatch(postRemoved(post)); + if (post.is_pinned) { + dispatch( + { + type: ChannelTypes.DECREMENT_PINNED_POST_COUNT, + id: post.channel_id, + } + ); + } + } + }; +} + +export function selectPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: PostTypes.RECEIVED_POST_SELECTED, + data: postId, + }); + + return {data: true}; + }; +} + +export function selectFocusedPostId(postId: string) { + return { + type: PostTypes.RECEIVED_FOCUSED_POST, + data: postId, + }; +} + +export function unflagPost(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + const preference = { + user_id: currentUserId, + category: Preferences.CATEGORY_FLAGGED_POST, + name: postId, + }; + + Client4.trackEvent('action', 'action_posts_unflag'); + + return deletePreferences(currentUserId, [preference])(dispatch, getState); + }; +} + +export function getOpenGraphMetadata(url: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getOpenGraphMetadata(url); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + if (data && (data.url || data.type || data.title || data.description)) { + dispatch({ + type: PostTypes.RECEIVED_OPEN_GRAPH_METADATA, + data, + url, + }); + } + + return {data}; + }; +} + +export function doPostAction(postId: string, actionId: string, selectedOption = '') { + return doPostActionWithCookie(postId, actionId, '', selectedOption); +} + +export function doPostActionWithCookie(postId: string, actionId: string, actionCookie: string, selectedOption = '') { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.doPostActionWithCookie(postId, actionId, actionCookie, selectedOption); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + if (data && data.trigger_id) { + dispatch({ + type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, + data: data.trigger_id, + }); + } + + return {data}; + }; +} + +export function addMessageIntoHistory(message: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: PostTypes.ADD_MESSAGE_INTO_HISTORY, + data: message, + }); + + return {data: true}; + }; +} + +export function resetHistoryIndex(index: number) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: PostTypes.RESET_HISTORY_INDEX, + data: index, + }); + + return {data: true}; + }; +} + +export function moveHistoryIndexBack(index: number) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: PostTypes.MOVE_HISTORY_INDEX_BACK, + data: index, + }); + + return {data: true}; + }; +} + +export function moveHistoryIndexForward(index: number) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: PostTypes.MOVE_HISTORY_INDEX_FORWARD, + data: index, + }); + + return {data: true}; + }; +} + +export function handleNewPost(msg: Omit) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const currentUserId = getCurrentUserId(state); + const post = JSON.parse(msg.data.post); + const myChannelMember = getMyChannelMemberSelector(state, post.channel_id); + const websocketMessageProps = msg.data; + + if (myChannelMember && Object.keys(myChannelMember).length === 0 && (myChannelMember as any).constructor === 'Object') { + await dispatch(getMyChannelMember(post.channel_id)); + } + + dispatch(completePostReceive(post, websocketMessageProps) as any); + + if (msg.data.channel_type === General.DM_CHANNEL) { + const otherUserId = getUserIdFromChannelName(currentUserId, msg.data.channel_name); + dispatch(makeDirectChannelVisibleIfNecessary(otherUserId)); + } else if (msg.data.channel_type === General.GM_CHANNEL) { + dispatch(makeGroupMessageVisibleIfNecessary(post.channel_id)); + } + + return {data: true}; + }; +} + +function completePostReceive(post: Post, websocketMessageProps: any) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const rootPost = Selectors.getPost(state, post.root_id); + + if (post.root_id && !rootPost) { + dispatch(getPostThread(post.root_id)); + } + + dispatch(lastPostActions(post, websocketMessageProps) as any); + }; +} + +export function lastPostActions(post: Post, websocketMessageProps: any) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const actions = [ + receivedNewPost(post), + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: post.channel_id + post.root_id, + userId: post.user_id, + now: Date.now(), + }, + }, + ]; + + await dispatch(batchActions(actions)); + + if (shouldIgnorePost(post)) { + return; + } + + let markAsRead = false; + let markAsReadOnServer = false; + if (!isManuallyUnread(getState(), post.channel_id)) { + if ( + post.user_id === getCurrentUserId(state) && + !isSystemMessage(post) && + !isFromWebhook(post) + ) { + markAsRead = true; + markAsReadOnServer = false; + } else if (post.channel_id === getCurrentChannelId(state)) { + markAsRead = true; + markAsReadOnServer = true; + } + } + + if (markAsRead) { + await dispatch(markChannelAsRead(post.channel_id, undefined, markAsReadOnServer)); + await dispatch(markChannelAsViewed(post.channel_id)); + } else { + await dispatch(markChannelAsUnread(websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions)); + } + }; +} diff --git a/app/mm-redux/actions/preferences.test.js b/app/mm-redux/actions/preferences.test.js new file mode 100644 index 000000000..d2954f9c1 --- /dev/null +++ b/app/mm-redux/actions/preferences.test.js @@ -0,0 +1,316 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/preferences'; +import {login} from '@mm-redux/actions/users'; +import {Client4} from '@mm-redux/client'; +import {Preferences, RequestStatus} from '../constants'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Preferences', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('getMyPreferences', async () => { + const user = TestHelper.basicUser; + const existingPreferences = [ + { + user_id: user.id, + category: 'test', + name: 'test1', + value: 'test', + }, + { + user_id: user.id, + category: 'test', + name: 'test2', + value: 'test', + }, + ]; + + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Client4.savePreferences(user.id, existingPreferences); + + nock(Client4.getUsersRoute()). + get('/me/preferences'). + reply(200, existingPreferences); + await Actions.getMyPreferences()(store.dispatch, store.getState); + + const state = store.getState(); + const {myPreferences} = state.entities.preferences; + + assert.ok(myPreferences['test--test1'], 'first preference doesn\'t exist'); + assert.deepEqual(existingPreferences[0], myPreferences['test--test1']); + assert.ok(myPreferences['test--test2'], 'second preference doesn\'t exist'); + assert.deepEqual(existingPreferences[1], myPreferences['test--test2']); + }); + + it('savePrefrences', async () => { + const user = TestHelper.basicUser; + const existingPreferences = [ + { + user_id: user.id, + category: 'test', + name: 'test1', + value: 'test', + }, + ]; + + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Client4.savePreferences(user.id, existingPreferences); + + nock(Client4.getUsersRoute()). + get('/me/preferences'). + reply(200, existingPreferences); + await Actions.getMyPreferences()(store.dispatch, store.getState); + + const preferences = [ + { + user_id: user.id, + category: 'test', + name: 'test2', + value: 'test', + }, + { + user_id: user.id, + category: 'test', + name: 'test3', + value: 'test', + }, + ]; + + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Actions.savePreferences(user.id, preferences)(store.dispatch, store.getState); + + const state = store.getState(); + const {myPreferences} = state.entities.preferences; + + assert.ok(myPreferences['test--test1'], 'first preference doesn\'t exist'); + assert.deepEqual(existingPreferences[0], myPreferences['test--test1']); + assert.ok(myPreferences['test--test2'], 'second preference doesn\'t exist'); + assert.deepEqual(preferences[0], myPreferences['test--test2']); + assert.ok(myPreferences['test--test3'], 'third preference doesn\'t exist'); + assert.deepEqual(preferences[1], myPreferences['test--test3']); + }); + + it('deletePreferences', async () => { + const user = TestHelper.basicUser; + const existingPreferences = [ + { + user_id: user.id, + category: 'test', + name: 'test1', + value: 'test', + }, + { + user_id: user.id, + category: 'test', + name: 'test2', + value: 'test', + }, + { + user_id: user.id, + category: 'test', + name: 'test3', + value: 'test', + }, + ]; + + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Client4.savePreferences(user.id, existingPreferences); + + nock(Client4.getUsersRoute()). + get('/me/preferences'). + reply(200, existingPreferences); + await Actions.getMyPreferences()(store.dispatch, store.getState); + + nock(Client4.getUsersRoute()). + post(`/${TestHelper.basicUser.id}/preferences/delete`). + reply(200, OK_RESPONSE); + await Actions.deletePreferences(user.id, [ + existingPreferences[0], + existingPreferences[2], + ])(store.dispatch, store.getState); + + const state = store.getState(); + const {myPreferences} = state.entities.preferences; + + assert.ok(!myPreferences['test--test1'], 'deleted preference still exists'); + assert.ok(myPreferences['test--test2'], 'second preference doesn\'t exist'); + assert.deepEqual(existingPreferences[1], myPreferences['test--test2']); + assert.ok(!myPreferences['test--test3'], 'third preference doesn\'t exist'); + }); + + it('makeDirectChannelVisibleIfNecessary', async () => { + const user = TestHelper.basicUser; + + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user2 = await TestHelper.createClient().createUser(TestHelper.fakeUser()); + + TestHelper.mockLogin(); + await login(user.email, 'password1')(store.dispatch, store.getState); + + // Test that a new preference is created if none exists + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Actions.makeDirectChannelVisibleIfNecessary(user2.id)(store.dispatch, store.getState); + + let state = store.getState(); + let myPreferences = state.entities.preferences.myPreferences; + let preference = myPreferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${user2.id}`]; + assert.ok(preference, 'preference for showing direct channel doesn\'t exist'); + assert.equal(preference.value, 'true', 'preference for showing direct channel is not true'); + + // Test that nothing changes if the preference already exists and is true + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Actions.makeDirectChannelVisibleIfNecessary(user2.id)(store.dispatch, store.getState); + + const state2 = store.getState(); + assert.equal(state, state2, 'store should not change since direct channel is already visible'); + + // Test that the preference is updated if it already exists and is false + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + Actions.savePreferences(user.id, [{ + ...preference, + value: 'false', + }])(store.dispatch, store.getState); + + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Actions.makeDirectChannelVisibleIfNecessary(user2.id)(store.dispatch, store.getState); + + state = store.getState(); + myPreferences = state.entities.preferences.myPreferences; + preference = myPreferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${user2.id}`]; + assert.ok(preference, 'preference for showing direct channel doesn\'t exist'); + assert.equal(preference.value, 'true', 'preference for showing direct channel is not true'); + }); + + it('saveTheme', async () => { + const user = TestHelper.basicUser; + const team = TestHelper.basicTeam; + const existingPreferences = [ + { + user_id: user.id, + category: 'theme', + name: team.id, + value: JSON.stringify({ + type: 'Mattermost', + }), + }, + ]; + + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Client4.savePreferences(user.id, existingPreferences); + + nock(Client4.getUsersRoute()). + get('/me/preferences'). + reply(200, existingPreferences); + await Actions.getMyPreferences()(store.dispatch, store.getState); + + const newTheme = { + type: 'Mattermost Dark', + }; + nock(Client4.getUsersRoute()). + put(`/${TestHelper.basicUser.id}/preferences`). + reply(200, OK_RESPONSE); + await Actions.saveTheme(team.id, newTheme)(store.dispatch, store.getState); + + const state = store.getState(); + const {myPreferences} = state.entities.preferences; + + assert.ok(myPreferences[`theme--${team.id}`], 'theme preference doesn\'t exist'); + assert.deepEqual(myPreferences[`theme--${team.id}`].value, JSON.stringify(newTheme)); + }); + + it('deleteTeamSpecificThemes', async () => { + const user = TestHelper.basicUser; + TestHelper.mockLogin(); + await login(user.email, user.password)(store.dispatch, store.getState); + + const theme = { + type: 'Mattermost Dark', + }; + const existingPreferences = [ + { + user_id: user.id, + category: 'theme', + name: '', + value: JSON.stringify(theme), + }, + { + user_id: user.id, + category: 'theme', + name: TestHelper.generateId(), + value: JSON.stringify({ + type: 'Mattermost', + }), + }, + { + user_id: user.id, + category: 'theme', + name: TestHelper.generateId(), + value: JSON.stringify({ + type: 'Mattermost', + }), + }, + ]; + + nock(Client4.getUsersRoute()). + put(`/${user.id}/preferences`). + reply(200, OK_RESPONSE); + await Client4.savePreferences(user.id, existingPreferences); + + nock(Client4.getUsersRoute()). + get('/me/preferences'). + reply(200, existingPreferences); + await Actions.getMyPreferences()(store.dispatch, store.getState); + + nock(Client4.getUsersRoute()). + post(`/${user.id}/preferences/delete`). + reply(200, OK_RESPONSE); + await Actions.deleteTeamSpecificThemes()(store.dispatch, store.getState); + + const state = store.getState(); + const {myPreferences} = state.entities.preferences; + + assert.equal(Object.entries(myPreferences).length, 1); + assert.ok(myPreferences['theme--'], 'theme preference doesn\'t exist'); + assert.equal(myPreferences['theme--'].value, JSON.stringify(theme)); + }); +}); diff --git a/app/mm-redux/actions/preferences.ts b/app/mm-redux/actions/preferences.ts new file mode 100644 index 000000000..aa566a5d1 --- /dev/null +++ b/app/mm-redux/actions/preferences.ts @@ -0,0 +1,159 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {Preferences} from '../constants'; +import {PreferenceTypes} from '@mm-redux/action_types'; +import {getMyPreferences as getMyPreferencesSelector, makeGetCategory} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; + +import {GetStateFunc, DispatchFunc, ActionFunc} from '@mm-redux/types/actions'; + +import {PreferenceType} from '@mm-redux/types/preferences'; + +import {bindClientFunc} from './helpers'; +import {getProfilesByIds, getProfilesInChannel} from './users'; +import {getChannelAndMyMember, getMyChannelMember} from './channels'; +export function deletePreferences(userId: string, preferences: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const myPreferences = getMyPreferencesSelector(state); + const currentPreferences = preferences.map((pref) => myPreferences[getPreferenceKey(pref.category, pref.name)]); + + dispatch({ + type: PreferenceTypes.DELETED_PREFERENCES, + data: preferences, + meta: { + offline: { + effect: () => Client4.deletePreferences(userId, preferences), + commit: { + type: PreferenceTypes.DELETED_PREFERENCES, + }, + rollback: { + type: PreferenceTypes.RECEIVED_PREFERENCES, + data: currentPreferences, + }, + }, + }, + }); + + return {data: true}; + }; +} + +export function getMyPreferences(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getMyPreferences, + onSuccess: PreferenceTypes.RECEIVED_ALL_PREFERENCES, + }); +} + +export function makeDirectChannelVisibleIfNecessary(otherUserId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const myPreferences = getMyPreferencesSelector(state); + const currentUserId = getCurrentUserId(state); + + let preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId)]; + + if (!preference || preference.value === 'false') { + preference = { + user_id: currentUserId, + category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + name: otherUserId, + value: 'true', + }; + getProfilesByIds([otherUserId])(dispatch, getState); + savePreferences(currentUserId, [preference])(dispatch); + } + + return {data: true}; + }; +} + +export function makeGroupMessageVisibleIfNecessary(channelId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const myPreferences = getMyPreferencesSelector(state); + const currentUserId = getCurrentUserId(state); + const {channels} = state.entities.channels; + + let preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId)]; + + if (!preference || preference.value === 'false') { + preference = { + user_id: currentUserId, + category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW, + name: channelId, + value: 'true', + }; + + if (channels[channelId]) { + getMyChannelMember(channelId)(dispatch, getState); + } else { + getChannelAndMyMember(channelId)(dispatch, getState); + } + + getProfilesInChannel(channelId, 0)(dispatch, getState); + savePreferences(currentUserId, [preference])(dispatch); + } + + return {data: true}; + }; +} + +export function savePreferences(userId: string, preferences: Array) { + return async (dispatch: DispatchFunc) => { + dispatch({ + type: PreferenceTypes.RECEIVED_PREFERENCES, + data: preferences, + meta: { + offline: { + effect: () => Client4.savePreferences(userId, preferences), + commit: { + type: PreferenceTypes.RECEIVED_PREFERENCES, + }, + rollback: { + type: PreferenceTypes.DELETED_PREFERENCES, + data: preferences, + }, + }, + }, + }); + + return {data: true}; + }; +} + +export function saveTheme(teamId: string, theme: {}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const currentUserId = getCurrentUserId(state); + const preference: PreferenceType = { + user_id: currentUserId, + category: Preferences.CATEGORY_THEME, + name: teamId || '', + value: JSON.stringify(theme), + }; + + await savePreferences(currentUserId, [preference])(dispatch); + return {data: true}; + }; +} + +export function deleteTeamSpecificThemes(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + + const getCategory: (state: any, preferenceId: string) => Array = makeGetCategory(); + const themePreferences: Array = getCategory(state, Preferences.CATEGORY_THEME); + const currentUserId = getCurrentUserId(state); + + const toDelete = themePreferences.filter((pref) => pref.name !== ''); + if (toDelete.length > 0) { + await deletePreferences(currentUserId, toDelete)(dispatch, getState); + } + + return {data: true}; + }; +} diff --git a/app/mm-redux/actions/roles.test.js b/app/mm-redux/actions/roles.test.js new file mode 100644 index 000000000..5c9e7a2fc --- /dev/null +++ b/app/mm-redux/actions/roles.test.js @@ -0,0 +1,119 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/roles'; +import {Client4} from '@mm-redux/client'; +import {RequestStatus} from '../constants'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.Roles', () => { + let store; + + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('getRolesByNames', async () => { + nock(Client4.getRolesRoute()). + post('/names'). + reply(200, [TestHelper.basicRoles.system_admin]); + await Actions.getRolesByNames(['system_admin'])(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.roles.getRolesByNames; + const {roles} = state.entities.roles; + + if (request.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(request.error)); + } + + assert.equal(roles.system_admin.name, 'system_admin'); + assert.deepEqual(roles.system_admin.permissions, TestHelper.basicRoles.system_admin.permissions); + }); + + it('getRoleByName', async () => { + nock(Client4.getRolesRoute()). + get('/name/system_admin'). + reply(200, TestHelper.basicRoles.system_admin); + await Actions.getRoleByName('system_admin')(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.roles.getRolesByNames; + const {roles} = state.entities.roles; + + if (request.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(request.error)); + } + + assert.equal(roles.system_admin.name, 'system_admin'); + assert.deepEqual(roles.system_admin.permissions, TestHelper.basicRoles.system_admin.permissions); + }); + + it('getRole', async () => { + nock(Client4.getRolesRoute()). + get('/' + TestHelper.basicRoles.system_admin.id). + reply(200, TestHelper.basicRoles.system_admin); + + await Actions.getRole(TestHelper.basicRoles.system_admin.id)(store.dispatch, store.getState); + + const state = store.getState(); + const request = state.requests.roles.getRole; + const {roles} = state.entities.roles; + + if (request.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(request.error)); + } + + assert.equal(roles.system_admin.name, 'system_admin'); + assert.deepEqual(roles.system_admin.permissions, TestHelper.basicRoles.system_admin.permissions); + }); + + it('loadRolesIfNeeded', async () => { + const mock1 = nock(Client4.getRolesRoute()). + post('/names', JSON.stringify(['existing_role'])). + reply(200, []); + const mock2 = nock(Client4.getRolesRoute()). + post('/names', JSON.stringify(['new_role'])). + reply(200, []); + + const fakeState = { + entities: { + roles: { + roles: { + existing_role: {}, + }, + }, + }, + }; + await Actions.loadRolesIfNeeded(['existing_role'])(store.dispatch, () => fakeState); + assert(!mock1.isDone()); + assert(!mock2.isDone()); + + await Actions.loadRolesIfNeeded(['existing_role', 'new_role', ''])(store.dispatch, () => fakeState); + assert(!mock1.isDone()); + assert(mock2.isDone()); + }); + + it('editRole', async () => { + const roleId = TestHelper.basicRoles.system_admin.id; + const mock = nock(Client4.getRolesRoute()). + put('/' + roleId + '/patch', JSON.stringify({id: roleId, test: 'test'})). + reply(200, {}); + + await Actions.editRole({id: roleId, test: 'test'})(store.dispatch, store.state); + assert(mock.isDone()); + }); +}); diff --git a/app/mm-redux/actions/roles.ts b/app/mm-redux/actions/roles.ts new file mode 100644 index 000000000..cd479b423 --- /dev/null +++ b/app/mm-redux/actions/roles.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {RoleTypes} from '@mm-redux/action_types'; +import {getRoles} from '@mm-redux/selectors/entities/roles_helpers'; +import {hasNewPermissions} from '@mm-redux/selectors/entities/general'; + +import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions'; +import {Role} from '@mm-redux/types/roles'; + +import {bindClientFunc} from './helpers'; +export function getRolesByNames(rolesNames: Array) { + return bindClientFunc({ + clientFunc: Client4.getRolesByNames, + onRequest: RoleTypes.ROLES_BY_NAMES_REQUEST, + onSuccess: [RoleTypes.RECEIVED_ROLES, RoleTypes.ROLES_BY_NAMES_SUCCESS], + onFailure: RoleTypes.ROLES_BY_NAMES_FAILURE, + params: [ + rolesNames, + ], + }); +} + +export function getRoleByName(roleName: string) { + return bindClientFunc({ + clientFunc: Client4.getRoleByName, + onRequest: RoleTypes.ROLE_BY_NAME_REQUEST, + onSuccess: [RoleTypes.RECEIVED_ROLE, RoleTypes.ROLE_BY_NAME_SUCCESS], + onFailure: RoleTypes.ROLE_BY_NAME_FAILURE, + params: [ + roleName, + ], + }); +} + +export function getRole(roleId: string) { + return bindClientFunc({ + clientFunc: Client4.getRole, + onRequest: RoleTypes.ROLE_BY_ID_REQUEST, + onSuccess: [RoleTypes.RECEIVED_ROLE, RoleTypes.ROLE_BY_ID_SUCCESS], + onFailure: RoleTypes.ROLE_BY_ID_FAILURE, + params: [ + roleId, + ], + }); +} + +export function editRole(role: Role) { + return bindClientFunc({ + clientFunc: Client4.patchRole, + onRequest: RoleTypes.EDIT_ROLE_REQUEST, + onSuccess: [RoleTypes.RECEIVED_ROLE, RoleTypes.EDIT_ROLE_SUCCESS], + onFailure: RoleTypes.EDIT_ROLE_FAILURE, + params: [ + role.id, + role, + ], + }); +} + +export function loadRolesIfNeeded(roles: Iterable): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const currentRoles = getRoles(state); + const newRoles = []; + + for (const role of roles) { + if (!currentRoles[role] && role.trim() !== '') { + newRoles.push(role); + } + } + if (newRoles.length > 0) { + return dispatch(getRolesByNames(newRoles)); + } + return {data: state.entities.roles.roles}; + }; +} diff --git a/app/mm-redux/actions/schemes.test.js b/app/mm-redux/actions/schemes.test.js new file mode 100644 index 000000000..e37e018c0 --- /dev/null +++ b/app/mm-redux/actions/schemes.test.js @@ -0,0 +1,104 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/schemes'; +import {Client4} from '@mm-redux/client'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.Schemes', () => { + let store; + + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('getSchemes', async () => { + const mockScheme = TestHelper.basicScheme; + + nock(Client4.getBaseRoute()). + get('/schemes'). + query(true). + reply(200, [mockScheme]); + + await Actions.getSchemes()(store.dispatch, store.getState); + const {schemes} = store.getState().entities.schemes; + + assert.ok(Object.keys(schemes).length > 0); + }); + + it('createScheme', async () => { + const mockScheme = TestHelper.basicScheme; + + nock(Client4.getBaseRoute()). + post('/schemes'). + reply(201, mockScheme); + await Actions.createScheme(TestHelper.mockScheme())(store.dispatch, store.getState); + + const {schemes} = store.getState().entities.schemes; + + const schemeId = Object.keys(schemes)[0]; + assert.strictEqual(Object.keys(schemes).length, 1); + assert.strictEqual(mockScheme.id, schemeId); + }); + + it('getScheme', async () => { + nock(Client4.getBaseRoute()). + get('/schemes/' + TestHelper.basicScheme.id). + reply(200, TestHelper.basicScheme); + + await Actions.getScheme(TestHelper.basicScheme.id)(store.dispatch, store.getState); + + const state = store.getState(); + const {schemes} = state.entities.schemes; + + assert.equal(schemes[TestHelper.basicScheme.id].name, TestHelper.basicScheme.name); + }); + + it('patchScheme', async () => { + const patchData = {name: 'The Updated Scheme', description: 'This is a scheme created by unit tests'}; + const scheme = { + ...TestHelper.basicScheme, + ...patchData, + }; + + nock(Client4.getBaseRoute()). + put('/schemes/' + TestHelper.basicScheme.id + '/patch'). + reply(200, scheme); + + await Actions.patchScheme(TestHelper.basicScheme.id, scheme)(store.dispatch, store.getState); + + const state = store.getState(); + const {schemes} = state.entities.schemes; + + const updated = schemes[TestHelper.basicScheme.id]; + assert.ok(updated); + assert.strictEqual(updated.name, patchData.name); + assert.strictEqual(updated.description, patchData.description); + }); + + it('deleteScheme', async () => { + nock(Client4.getBaseRoute()). + delete('/schemes/' + TestHelper.basicScheme.id). + reply(200, {status: 'OK'}); + + await Actions.deleteScheme(TestHelper.basicScheme.id)(store.dispatch, store.getState); + + const state = store.getState(); + const {schemes} = state.entities.schemes; + + assert.notStrictEqual(schemes, {}); + }); +}); diff --git a/app/mm-redux/actions/schemes.ts b/app/mm-redux/actions/schemes.ts new file mode 100644 index 000000000..72c411198 --- /dev/null +++ b/app/mm-redux/actions/schemes.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {SchemeTypes} from '@mm-redux/action_types'; +import {General} from '../constants'; + +import {Scheme, SchemeScope, SchemePatch} from '@mm-redux/types/schemes'; + +import {ActionFunc, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; + +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +import {logError} from './errors'; +export function getScheme(schemeId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getScheme, + onSuccess: [SchemeTypes.RECEIVED_SCHEME], + params: [ + schemeId, + ], + }); +} + +export function getSchemes(scope: SchemeScope, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getSchemes, + onSuccess: [SchemeTypes.RECEIVED_SCHEMES], + params: [ + scope, + page, + perPage, + ], + }); +} + +export function createScheme(scheme: Scheme): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createScheme, + onSuccess: [SchemeTypes.CREATED_SCHEME], + params: [ + scheme, + ], + }); +} + +export function deleteScheme(schemeId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data = null; + try { + data = await Client4.deleteScheme(schemeId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({type: SchemeTypes.DELETED_SCHEME, data: {schemeId}}); + + return {data}; + }; +} + +export function patchScheme(schemeId: string, scheme: SchemePatch): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.patchScheme, + onSuccess: [SchemeTypes.PATCHED_SCHEME], + params: [ + schemeId, + scheme, + ], + }); +} + +export function getSchemeTeams(schemeId: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getSchemeTeams, + onSuccess: [SchemeTypes.RECEIVED_SCHEME_TEAMS], + params: [ + schemeId, + page, + perPage, + ], + }); +} + +export function getSchemeChannels(schemeId: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getSchemeChannels, + onSuccess: [SchemeTypes.RECEIVED_SCHEME_CHANNELS], + params: [ + schemeId, + page, + perPage, + ], + }); +} diff --git a/app/mm-redux/actions/search.test.js b/app/mm-redux/actions/search.test.js new file mode 100644 index 000000000..5e903f4ee --- /dev/null +++ b/app/mm-redux/actions/search.test.js @@ -0,0 +1,120 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import * as Actions from '@mm-redux/actions/search'; +import {Client4} from '@mm-redux/client'; + +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +describe('Actions.Search', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('Perform Search', async () => { + const {dispatch, getState} = store; + + let post1 = { + ...TestHelper.fakePost(TestHelper.basicChannel.id), + message: 'try searching for this using the first and last word', + }; + + let post2 = { + ...TestHelper.fakePost(TestHelper.basicChannel.id), + message: 'return this message in second attempt', + }; + + nock(Client4.getBaseRoute()). + post('/posts'). + reply(201, {...post1, id: TestHelper.generateId()}); + post1 = await Client4.createPost(post1); + + nock(Client4.getBaseRoute()). + post('/posts'). + reply(201, {...post2, id: TestHelper.generateId()}); + post2 = await Client4.createPost(post2); + + // Test for a couple of words + const search1 = 'try word'; + + nock(Client4.getTeamsRoute()). + post(`/${TestHelper.basicTeam.id}/posts/search`). + reply(200, {order: [post1.id], posts: {[post1.id]: post1}}); + nock(Client4.getChannelsRoute()). + get(`/${TestHelper.basicChannel.id}/members/me`). + reply(201, {user_id: TestHelper.basicUser.id, channel_id: TestHelper.basicChannel.id}); + + await Actions.searchPosts(TestHelper.basicTeam.id, search1)(dispatch, getState); + + const state = getState(); + const {recent, results} = state.entities.search; + const {posts} = state.entities.posts; + const current = state.entities.search.current[TestHelper.basicTeam.id]; + assert.ok(recent[TestHelper.basicTeam.id]); + const searchIsPresent = recent[TestHelper.basicTeam.id].findIndex((r) => r.terms === search1); + assert.ok(searchIsPresent !== -1); + assert.equal(Object.keys(recent[TestHelper.basicTeam.id]).length, 1); + assert.equal(results.length, 1); + assert.ok(posts[results[0]]); + assert.ok(current.isEnd); + + // DISABLED + // Test for posts from a user in a channel + //const search2 = `from: ${TestHelper.basicUser.username} in: ${TestHelper.basicChannel.name}`; + + //nock(Client4.getTeamsRoute(), `/${TestHelper.basicTeam.id}/posts/search`). + //post(`/${TestHelper.basicTeam.id}/posts/search`). + //reply(200, {order: [post1.id, post2.id, TestHelper.basicPost.id], posts: {[post1.id]: post1, [TestHelper.basicPost.id]: TestHelper.basicPost, [post2.id]: post2}}); + //nock(Client4.getChannelsRoute()). + //get(`/${TestHelper.basicChannel.id}/members/me`). + //reply(201, {user_id: TestHelper.basicUser.id, channel_id: TestHelper.basicChannel.id}); + // + //await Actions.searchPosts( + //TestHelper.basicTeam.id, + //search2 + //)(dispatch, getState); + // + //state = getState(); + //recent = state.entities.search.recent; + //results = state.entities.search.results; + //searchIsPresent = recent[TestHelper.basicTeam.id].findIndex((r) => r.terms === search1); + //assert.ok(searchIsPresent !== -1); + //assert.equal(Object.keys(recent[TestHelper.basicTeam.id]).length, 2); + //assert.equal(results.length, 3); + + // Clear posts from the search store + //await Actions.clearSearch()(dispatch, getState); + //state = getState(); + //recent = state.entities.search.recent; + //results = state.entities.search.results; + //searchIsPresent = recent[TestHelper.basicTeam.id].findIndex((r) => r.terms === search1); + //assert.ok(searchIsPresent !== -1); + //assert.equal(Object.keys(recent[TestHelper.basicTeam.id]).length, 2); + //assert.equal(results.length, 0); + + // Clear a recent term + //await Actions.removeSearchTerms(TestHelper.basicTeam.id, search2)(dispatch, getState); + //state = getState(); + //recent = state.entities.search.recent; + //results = state.entities.search.results; + //searchIsPresent = recent[TestHelper.basicTeam.id].findIndex((r) => r.terms === search1); + //assert.ok(searchIsPresent !== -1); + //searchIsPresent = recent[TestHelper.basicTeam.id].findIndex((r) => r.terms === search2); + //assert.ok(searchIsPresent === -1); + //assert.equal(Object.keys(recent[TestHelper.basicTeam.id]).length, 1); + //assert.equal(results.length, 0); + }); +}); diff --git a/app/mm-redux/actions/search.ts b/app/mm-redux/actions/search.ts new file mode 100644 index 000000000..88479b9e1 --- /dev/null +++ b/app/mm-redux/actions/search.ts @@ -0,0 +1,256 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {SearchTypes} from '@mm-redux/action_types'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId, getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users'; + +import {getChannelAndMyMember, getChannelMembers} from './channels'; +import {forceLogoutIfNecessary} from './helpers'; +import {logError} from './errors'; +import {getProfilesAndStatusesForPosts, receivedPosts} from './posts'; +import {ActionResult, batchActions, DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions'; +import {RelationOneToOne} from '@mm-redux/types/utilities'; +import {Post} from '@mm-redux/types/posts'; +import {SearchParameter} from '@mm-redux/types/search'; +const WEBAPP_SEARCH_PER_PAGE = 20; +export function getMissingChannelsFromPosts(posts: RelationOneToOne): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const { + channels, + membersInChannel, + myMembers, + } = getState().entities.channels; + const promises: Promise[] = []; + Object.values(posts).forEach((post) => { + const id = post.channel_id; + + if (!channels[id] || !myMembers[id]) { + promises.push(dispatch(getChannelAndMyMember(id))); + } + + if (!membersInChannel[id]) { + promises.push(dispatch(getChannelMembers(id))); + } + }); + return Promise.all(promises); + }; +} + +export function searchPostsWithParams(teamId: string, params: SearchParameter): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const isGettingMore = params.page > 0; + dispatch({ + type: SearchTypes.SEARCH_POSTS_REQUEST, + isGettingMore, + }); + let posts; + + try { + posts = await Client4.searchPostsWithParams(teamId, params); + + const profilesAndStatuses = getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + const missingChannels = dispatch(getMissingChannelsFromPosts(posts.posts)); + const arr: [Promise, Promise] = [profilesAndStatuses, missingChannels]; + await Promise.all(arr); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: posts, + isGettingMore, + }, + receivedPosts(posts), + { + type: SearchTypes.RECEIVED_SEARCH_TERM, + data: { + teamId, + params, + isEnd: (posts.order.length < params.per_page), + }, + }, + { + type: SearchTypes.SEARCH_POSTS_SUCCESS, + }, + ], 'SEARCH_POST_BATCH')); + + return {data: posts}; + }; +} + +export function searchPosts(teamId: string, terms: string, isOrSearch: boolean, includeDeletedChannels: boolean) { + return searchPostsWithParams(teamId, {terms, is_or_search: isOrSearch, include_deleted_channels: includeDeletedChannels, page: 0, per_page: WEBAPP_SEARCH_PER_PAGE}); +} + +export function getMorePostsForSearch(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const teamId = getCurrentTeamId(getState()); + const {params, isEnd} = getState().entities.search.current[teamId]; + if (!isEnd) { + const newParams = Object.assign({}, params); + newParams.page += 1; + return dispatch(searchPostsWithParams(teamId, newParams)); + } + return {data: true}; + }; +} + +export function clearSearch(): ActionFunc { + return async (dispatch) => { + dispatch({type: SearchTypes.REMOVE_SEARCH_POSTS}); + + return {data: true}; + }; +} + +export function getFlaggedPosts(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const userId = getCurrentUserId(state); + const teamId = getCurrentTeamId(state); + + dispatch({type: SearchTypes.SEARCH_FLAGGED_POSTS_REQUEST}); + + let posts; + try { + posts = await Client4.getFlaggedPosts(userId, '', teamId); + + await Promise.all([getProfilesAndStatusesForPosts(posts.posts, dispatch, getState) as any, dispatch(getMissingChannelsFromPosts(posts.posts)) as any]); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: SearchTypes.SEARCH_FLAGGED_POSTS_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + { + type: SearchTypes.RECEIVED_SEARCH_FLAGGED_POSTS, + data: posts, + }, + receivedPosts(posts), + { + type: SearchTypes.SEARCH_FLAGGED_POSTS_SUCCESS, + }, + ], 'SEARCH_FLAGGED_POSTS_BATCH')); + + return {data: posts}; + }; +} + +export function getPinnedPosts(channelId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: SearchTypes.SEARCH_PINNED_POSTS_REQUEST}); + + let result; + try { + result = await Client4.getPinnedPosts(channelId); + + const profilesAndStatuses = getProfilesAndStatusesForPosts(result.posts, dispatch, getState); + const missingChannels = dispatch(getMissingChannelsFromPosts(result.posts)); + const arr: [Promise, Promise] = [profilesAndStatuses, missingChannels]; + await Promise.all(arr); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: SearchTypes.SEARCH_PINNED_POSTS_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + { + type: SearchTypes.RECEIVED_SEARCH_PINNED_POSTS, + data: { + pinned: result, + channelId, + }, + }, + receivedPosts(result), + { + type: SearchTypes.SEARCH_PINNED_POSTS_SUCCESS, + }, + ], 'SEARCH_PINNED_POSTS_BATCH')); + + return {data: result}; + }; +} + +export function clearPinnedPosts(channelId: string): ActionFunc { + return async (dispatch) => { + dispatch({ + type: SearchTypes.REMOVE_SEARCH_PINNED_POSTS, + data: { + channelId, + }, + }); + + return {data: true}; + }; +} + +export function getRecentMentions(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const teamId = getCurrentTeamId(state); + + let posts; + try { + const termKeys = getCurrentUserMentionKeys(state).filter(({key}) => { + return key !== '@channel' && key !== '@all' && key !== '@here'; + }); + + const terms = termKeys.map(({key}) => key).join(' ').trim() + ' '; + + Client4.trackEvent('api', 'api_posts_search_mention'); + posts = await Client4.searchPosts(teamId, terms, true); + + const profilesAndStatuses = getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); + const missingChannels = dispatch(getMissingChannelsFromPosts(posts.posts)); + const arr: [Promise, Promise] = [profilesAndStatuses, missingChannels]; + await Promise.all(arr); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: posts, + }, + receivedPosts(posts), + ], 'SEARCH_RECENT_MENTIONS_BATCH')); + + return {data: posts}; + }; +} + +export function removeSearchTerms(teamId: string, terms: string): ActionFunc { + return async (dispatch) => { + dispatch({ + type: SearchTypes.REMOVE_SEARCH_TERM, + data: { + teamId, + terms, + }, + }); + + return {data: true}; + }; +} + +export default { + clearSearch, + removeSearchTerms, + searchPosts, +}; diff --git a/app/mm-redux/actions/teams.test.js b/app/mm-redux/actions/teams.test.js new file mode 100644 index 000000000..e631d4466 --- /dev/null +++ b/app/mm-redux/actions/teams.test.js @@ -0,0 +1,770 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; +import fs from 'fs'; + +import * as Actions from '@mm-redux/actions/teams'; +import {login} from '@mm-redux/actions/users'; +import {Client4} from '@mm-redux/client'; +import {General, RequestStatus} from '../constants'; +import {GeneralTypes} from '@mm-redux/action_types'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Teams', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('selectTeam', async () => { + await Actions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState); + await TestHelper.wait(100); + const {currentTeamId} = store.getState().entities.teams; + + assert.ok(currentTeamId); + assert.equal(currentTeamId, TestHelper.basicTeam.id); + }); + + it('getMyTeams', async () => { + TestHelper.mockLogin(); + await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/users/me/teams'). + reply(200, [TestHelper.basicTeam]); + await Actions.getMyTeams()(store.dispatch, store.getState); + + const teamsRequest = store.getState().requests.teams.getMyTeams; + const {teams} = store.getState().entities.teams; + + if (teamsRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(teamsRequest.error)); + } + + assert.ok(teams); + assert.ok(teams[TestHelper.basicTeam.id]); + }); + + it('getTeamsForUser', async () => { + nock(Client4.getBaseRoute()). + get(`/users/${TestHelper.basicUser.id}/teams`). + reply(200, [TestHelper.basicTeam]); + + await Actions.getTeamsForUser(TestHelper.basicUser.id)(store.dispatch, store.getState); + + const teamsRequest = store.getState().requests.teams.getTeams; + const {teams} = store.getState().entities.teams; + + if (teamsRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(teamsRequest.error)); + } + + assert.ok(teams); + assert.ok(teams[TestHelper.basicTeam.id]); + }); + + it('getTeams', async () => { + let team = {...TestHelper.fakeTeam(), allow_open_invite: true}; + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, {...team, id: TestHelper.generateId()}); + team = await Client4.createTeam(team); + + nock(Client4.getBaseRoute()). + get('/teams'). + query(true). + reply(200, [team]); + await Actions.getTeams()(store.dispatch, store.getState); + + const teamsRequest = store.getState().requests.teams.getTeams; + const {teams} = store.getState().entities.teams; + + if (teamsRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(teamsRequest.error)); + } + + assert.ok(Object.keys(teams).length > 0); + }); + + it('getTeams with total count', async () => { + let team = {...TestHelper.fakeTeam(), allow_open_invite: true}; + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, {...team, id: TestHelper.generateId()}); + team = await Client4.createTeam(team); + + nock(Client4.getBaseRoute()). + get('/teams'). + query(true). + reply(200, {teams: [team], total_count: 43}); + await Actions.getTeams(0, 1, true)(store.dispatch, store.getState); + + const teamsRequest = store.getState().requests.teams.getTeams; + const {teams, totalCount} = store.getState().entities.teams; + + if (teamsRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(teamsRequest.error)); + } + + assert.ok(Object.keys(teams).length > 0); + assert.equal(totalCount, 43); + }); + + it('getTeam', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + const team = await Client4.createTeam(TestHelper.fakeTeam()); + + nock(Client4.getBaseRoute()). + get(`/teams/${team.id}`). + reply(200, team); + await Actions.getTeam(team.id)(store.dispatch, store.getState); + + const state = store.getState(); + const {teams} = state.entities.teams; + + assert.ok(teams); + assert.ok(teams[team.id]); + }); + + it('getTeamByName', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + const team = await Client4.createTeam(TestHelper.fakeTeam()); + + nock(Client4.getBaseRoute()). + get(`/teams/name/${team.name}`). + reply(200, team); + await Actions.getTeamByName(team.name)(store.dispatch, store.getState); + + const state = store.getState(); + const {teams} = state.entities.teams; + + assert.ok(teams); + assert.ok(teams[team.id]); + }); + + it('createTeam', async () => { + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + await Actions.createTeam( + TestHelper.fakeTeam(), + )(store.dispatch, store.getState); + + const {teams, myMembers, currentTeamId} = store.getState().entities.teams; + + const teamId = Object.keys(teams)[0]; + assert.strictEqual(Object.keys(teams).length, 1); + assert.strictEqual(currentTeamId, teamId); + assert.ok(myMembers[teamId]); + }); + + it('deleteTeam', async () => { + const secondClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + await secondClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + const secondTeam = await secondClient.createTeam( + TestHelper.fakeTeam()); + + nock(Client4.getBaseRoute()). + delete(`/teams/${secondTeam.id}`). + reply(200, OK_RESPONSE); + + await Actions.deleteTeam( + secondTeam.id, + )(store.dispatch, store.getState); + + const {teams, myMembers} = store.getState().entities.teams; + assert.ifError(teams[secondTeam.id]); + assert.ifError(myMembers[secondTeam.id]); + }); + + it('updateTeam', async () => { + const displayName = 'The Updated Team'; + const description = 'This is a team created by unit tests'; + const team = { + ...TestHelper.basicTeam, + display_name: displayName, + description, + }; + + nock(Client4.getBaseRoute()). + put(`/teams/${team.id}`). + reply(200, team); + await Actions.updateTeam(team)(store.dispatch, store.getState); + + const {teams} = store.getState().entities.teams; + const updated = teams[TestHelper.basicTeam.id]; + + assert.ok(updated); + assert.strictEqual(updated.display_name, displayName); + assert.strictEqual(updated.description, description); + }); + + it('patchTeam', async () => { + const displayName = 'The Patched Team'; + const description = 'This is a team created by unit tests'; + const team = { + ...TestHelper.basicTeam, + display_name: displayName, + description, + }; + + nock(Client4.getBaseRoute()). + put(`/teams/${team.id}/patch`). + reply(200, team); + await Actions.patchTeam(team)(store.dispatch, store.getState); + const {teams} = store.getState().entities.teams; + + const patched = teams[TestHelper.basicTeam.id]; + + assert.ok(patched); + assert.strictEqual(patched.display_name, displayName); + assert.strictEqual(patched.description, description); + }); + + it('Join Open Team', async () => { + const client = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + const user = await client.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + await client.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, {...TestHelper.fakeTeamWithId(), allow_open_invite: true}); + const team = await client.createTeam({...TestHelper.fakeTeam(), allow_open_invite: true}); + + store.dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: '4.0.0'}); + + nock(Client4.getBaseRoute()). + post('/teams/members/invite'). + query(true). + reply(201, {user_id: TestHelper.basicUser.id, team_id: team.id}); + + nock(Client4.getBaseRoute()). + get(`/teams/${team.id}`). + reply(200, team); + + nock(Client4.getUserRoute('me')). + get('/teams/members'). + reply(200, [{user_id: TestHelper.basicUser.id, roles: 'team_user', team_id: team.id}]); + + nock(Client4.getUserRoute('me')). + get('/teams/unread'). + reply(200, [{team_id: team.id, msg_count: 0, mention_count: 0}]); + + await Actions.joinTeam(team.invite_id, team.id)(store.dispatch, store.getState); + + const state = store.getState(); + + const request = state.requests.teams.joinTeam; + + if (request.status !== RequestStatus.SUCCESS) { + throw new Error(JSON.stringify(request.error)); + } + + const {teams, myMembers} = state.entities.teams; + assert.ok(teams[team.id]); + assert.ok(myMembers[team.id]); + }); + + it('getMyTeamMembers and getMyTeamUnreads', async () => { + nock(Client4.getUserRoute('me')). + get('/teams/members'). + reply(200, [{user_id: TestHelper.basicUser.id, roles: 'team_user', team_id: TestHelper.basicTeam.id}]); + await Actions.getMyTeamMembers()(store.dispatch, store.getState); + + nock(Client4.getUserRoute('me')). + get('/teams/unread'). + reply(200, [{team_id: TestHelper.basicTeam.id, msg_count: 0, mention_count: 0}]); + await Actions.getMyTeamUnreads()(store.dispatch, store.getState); + + const members = store.getState().entities.teams.myMembers; + const member = members[TestHelper.basicTeam.id]; + + assert.ok(member); + assert.ok(Object.prototype.hasOwnProperty.call(member, 'mention_count')); + }); + + it('getTeamMembersForUser', async () => { + nock(Client4.getUserRoute(TestHelper.basicUser.id)). + get('/teams/members'). + reply(200, [{user_id: TestHelper.basicUser.id, team_id: TestHelper.basicTeam.id}]); + await Actions.getTeamMembersForUser(TestHelper.basicUser.id)(store.dispatch, store.getState); + + const membersInTeam = store.getState().entities.teams.membersInTeam; + + assert.ok(membersInTeam); + assert.ok(membersInTeam[TestHelper.basicTeam.id]); + assert.ok(membersInTeam[TestHelper.basicTeam.id][TestHelper.basicUser.id]); + }); + + it('getTeamMember', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + get(`/teams/${TestHelper.basicTeam.id}/members/${user.id}`). + reply(200, {user_id: user.id, team_id: TestHelper.basicTeam.id}); + await Actions.getTeamMember(TestHelper.basicTeam.id, user.id)(store.dispatch, store.getState); + + const members = store.getState().entities.teams.membersInTeam; + + assert.ok(members[TestHelper.basicTeam.id]); + assert.ok(members[TestHelper.basicTeam.id][user.id]); + }); + + it('getTeamMembers', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user1 = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user2 = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + post('/members'). + reply(201, {user_id: user1.id, team_id: TestHelper.basicTeam.id}); + const {data: member1} = await Actions.addUserToTeam(TestHelper.basicTeam.id, user1.id)(store.dispatch, store.getState); + + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + post('/members'). + reply(201, {user_id: user2.id, team_id: TestHelper.basicTeam.id}); + const {data: member2} = await Actions.addUserToTeam(TestHelper.basicTeam.id, user2.id)(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/teams/${TestHelper.basicTeam.id}/members`). + query(true). + reply(200, [member1, member2, TestHelper.basicTeamMember]); + await Actions.getTeamMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState); + const membersInTeam = store.getState().entities.teams.membersInTeam; + + assert.ok(membersInTeam[TestHelper.basicTeam.id]); + assert.ok(membersInTeam[TestHelper.basicTeam.id][TestHelper.basicUser.id]); + assert.ok(membersInTeam[TestHelper.basicTeam.id][user1.id]); + assert.ok(membersInTeam[TestHelper.basicTeam.id][user2.id]); + }); + + it('getTeamMembersByIds', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + const user1 = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + const user2 = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post(`/teams/${TestHelper.basicTeam.id}/members/ids`). + reply(200, [{user_id: user1.id, team_id: TestHelper.basicTeam.id}, {user_id: user2.id, team_id: TestHelper.basicTeam.id}]); + await Actions.getTeamMembersByIds( + TestHelper.basicTeam.id, + [user1.id, user2.id], + )(store.dispatch, store.getState); + + const members = store.getState().entities.teams.membersInTeam; + + assert.ok(members[TestHelper.basicTeam.id]); + assert.ok(members[TestHelper.basicTeam.id][user1.id]); + assert.ok(members[TestHelper.basicTeam.id][user2.id]); + }); + + it('getTeamStats', async () => { + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + get('/stats'). + reply(200, {team_id: TestHelper.basicTeam.id, total_member_count: 2605, active_member_count: 2571}); + await Actions.getTeamStats(TestHelper.basicTeam.id)(store.dispatch, store.getState); + + const {stats} = store.getState().entities.teams; + + const stat = stats[TestHelper.basicTeam.id]; + assert.ok(stat); + + assert.ok(stat.total_member_count > 1); + assert.ok(stat.active_member_count > 1); + }); + + it('addUserToTeam', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + post('/members'). + reply(201, {user_id: user.id, team_id: TestHelper.basicTeam.id}); + await Actions.addUserToTeam(TestHelper.basicTeam.id, user.id)(store.dispatch, store.getState); + const members = store.getState().entities.teams.membersInTeam; + + assert.ok(members[TestHelper.basicTeam.id]); + assert.ok(members[TestHelper.basicTeam.id][user.id]); + }); + + it('addUsersToTeam', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user2 = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + post('/members/batch'). + reply(201, [{user_id: user.id, team_id: TestHelper.basicTeam.id}, {user_id: user2.id, team_id: TestHelper.basicTeam.id}]); + await Actions.addUsersToTeam(TestHelper.basicTeam.id, [user.id, user2.id])(store.dispatch, store.getState); + + const members = store.getState().entities.teams.membersInTeam; + const profilesInTeam = store.getState().entities.users.profilesInTeam; + + assert.ok(members[TestHelper.basicTeam.id]); + assert.ok(members[TestHelper.basicTeam.id][user.id]); + assert.ok(members[TestHelper.basicTeam.id][user2.id]); + assert.ok(profilesInTeam[TestHelper.basicTeam.id]); + assert.ok(profilesInTeam[TestHelper.basicTeam.id].has(user.id)); + assert.ok(profilesInTeam[TestHelper.basicTeam.id].has(user2.id)); + }); + + describe('removeUserFromTeam', () => { + const team = {id: 'team'}; + const user = {id: 'user'}; + + test('should remove the user from the team', async () => { + store = await configureStore({ + entities: { + teams: { + membersInTeam: { + [team.id]: { + [user.id]: {}, + }, + }, + }, + users: { + currentUserId: '', + profilesInTeam: { + [team.id]: [user.id], + }, + profilesNotInTeam: { + [team.id]: [], + }, + }, + }, + }); + + nock(Client4.getBaseRoute()). + delete(`/teams/${team.id}/members/${user.id}`). + reply(200, OK_RESPONSE); + await store.dispatch(Actions.removeUserFromTeam(team.id, user.id)); + + const state = store.getState(); + expect(state.entities.teams.membersInTeam[team.id]).toEqual({}); + expect(state.entities.users.profilesInTeam[team.id]).toEqual(new Set()); + expect(state.entities.users.profilesNotInTeam[team.id]).toEqual(new Set([user.id])); + }); + + test('should leave all channels when leaving a team', async () => { + const channel1 = {id: 'channel1', team_id: team.id}; + const channel2 = {id: 'channel2', team_id: 'team2'}; + + store = await configureStore({ + entities: { + channels: { + channels: { + [channel1.id]: channel1, + [channel2.id]: channel2, + }, + myMembers: { + [channel1.id]: {user_id: user.id, channel_id: channel1.id}, + [channel2.id]: {user_id: user.id, channel_id: channel2.id}, + }, + }, + users: { + currentUserId: user.id, + }, + }, + }); + + nock(Client4.getBaseRoute()). + delete(`/teams/${team.id}/members/${user.id}`). + reply(200, OK_RESPONSE); + await store.dispatch(Actions.removeUserFromTeam(team.id, user.id)); + + const state = store.getState(); + expect(state.entities.channels.myMembers[channel1.id]).toBeFalsy(); + expect(state.entities.channels.myMembers[channel2.id]).toBeTruthy(); + }); + + test('should clear the current channel when leaving a team', async () => { + const channel = {id: 'channel'}; + + store = await configureStore({ + entities: { + channels: { + channels: { + [channel.id]: channel, + }, + myMembers: {}, + }, + users: { + currentUserId: user.id, + }, + }, + }); + + nock(Client4.getBaseRoute()). + delete(`/teams/${team.id}/members/${user.id}`). + reply(200, OK_RESPONSE); + await store.dispatch(Actions.removeUserFromTeam(team.id, user.id)); + + const state = store.getState(); + expect(state.entities.channels.currentChannelId).toBe(''); + }); + }); + + it('updateTeamMemberRoles', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, TestHelper.fakeUserWithId()); + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + post('/members'). + reply(201, {user_id: user.id, team_id: TestHelper.basicTeam.id}); + await Actions.addUserToTeam(TestHelper.basicTeam.id, user.id)(store.dispatch, store.getState); + + const roles = General.TEAM_USER_ROLE + ' ' + General.TEAM_ADMIN_ROLE; + + nock(Client4.getBaseRoute()). + put(`/teams/${TestHelper.basicTeam.id}/members/${user.id}/roles`). + reply(200, {user_id: user.id, team_id: TestHelper.basicTeam.id, roles}); + await Actions.updateTeamMemberRoles(TestHelper.basicTeam.id, user.id, roles)(store.dispatch, store.getState); + + const members = store.getState().entities.teams.membersInTeam; + + assert.ok(members[TestHelper.basicTeam.id]); + assert.ok(members[TestHelper.basicTeam.id][user.id]); + assert.ok(members[TestHelper.basicTeam.id][user.id].roles === roles); + }); + + it('sendEmailInvitesToTeam', async () => { + nock(Client4.getTeamRoute(TestHelper.basicTeam.id)). + post('/invite/email'). + reply(200, OK_RESPONSE); + const {data} = await Actions.sendEmailInvitesToTeam(TestHelper.basicTeam.id, ['fakeemail1@example.com', 'fakeemail2@example.com'])(store.dispatch, store.getState); + assert.deepEqual(data, OK_RESPONSE); + }); + + it('checkIfTeamExists', async () => { + nock(Client4.getBaseRoute()). + get(`/teams/name/${TestHelper.basicTeam.name}/exists`). + reply(200, {exists: true}); + + let {data: exists} = await Actions.checkIfTeamExists(TestHelper.basicTeam.name)(store.dispatch, store.getState); + + assert.ok(exists === true); + + nock(Client4.getBaseRoute()). + get('/teams/name/junk/exists'). + reply(200, {exists: false}); + const {data} = await Actions.checkIfTeamExists('junk')(store.dispatch, store.getState); + exists = data; + + assert.ok(exists === false); + }); + + it('setTeamIcon', async () => { + TestHelper.mockLogin(); + await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const team = TestHelper.basicTeam; + const imageData = fs.createReadStream('test/assets/images/test.png'); + + nock(Client4.getTeamRoute(team.id)). + post('/image'). + reply(200, OK_RESPONSE); + + const {data} = await Actions.setTeamIcon(team.id, imageData)(store.dispatch, store.getState); + assert.deepEqual(data, OK_RESPONSE); + }); + + it('removeTeamIcon', async () => { + TestHelper.mockLogin(); + await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const team = TestHelper.basicTeam; + + nock(Client4.getTeamRoute(team.id)). + delete('/image'). + reply(200, OK_RESPONSE); + + const {data} = await Actions.removeTeamIcon(team.id)(store.dispatch, store.getState); + assert.deepEqual(data, OK_RESPONSE); + }); + + it('updateTeamScheme', async () => { + TestHelper.mockLogin(); + await login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const schemeId = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'; + const {id} = TestHelper.basicTeam; + + nock(Client4.getBaseRoute()). + put('/teams/' + id + '/scheme'). + reply(200, OK_RESPONSE); + + await Actions.updateTeamScheme(id, schemeId)(store.dispatch, store.getState); + + const state = store.getState(); + const {teams} = state.entities.teams; + + const updated = teams[id]; + assert.ok(updated); + assert.equal(updated.scheme_id, schemeId); + }); + + it('membersMinusGroupMembers', async () => { + const teamID = 'tid10000000000000000000000'; + const groupIDs = ['gid10000000000000000000000', 'gid20000000000000000000000']; + const page = 4; + const perPage = 63; + + nock(Client4.getBaseRoute()).get( + `/teams/${teamID}/members_minus_group_members?group_ids=${groupIDs.join(',')}&page=${page}&per_page=${perPage}`). + reply(200, {users: [], total_count: 0}); + + const {error} = await Actions.membersMinusGroupMembers(teamID, groupIDs, page, perPage)(store.dispatch, store.getState); + + assert.equal(error, null); + }); + + it('searchTeams', async () => { + const userClient = TestHelper.createClient(); + + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(201, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, user); + + await userClient.login(user.email, 'password1'); + + nock(Client4.getBaseRoute()). + post('/teams'). + reply(201, TestHelper.fakeTeamWithId()); + + const userTeam = await userClient.createTeam( + TestHelper.fakeTeam(), + ); + + nock(Client4.getBaseRoute()). + post('/teams/search'). + reply(200, [TestHelper.basicTeam, userTeam]); + + const {data} = await store.dispatch(Actions.searchTeams('test', 0)); + + const moreRequest = store.getState().requests.teams.getTeams; + if (moreRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(moreRequest.error)); + } + + nock(Client4.getBaseRoute()). + post('/teams/search'). + reply(200, {teams: [TestHelper.basicTeam, userTeam], total_count: 2}); + + const response = await store.dispatch(Actions.searchTeams('test', '', false, true)); + + const paginatedRequest = store.getState().requests.teams.getTeams; + if (paginatedRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(paginatedRequest.error)); + } + + assert.ok(response.data.teams.length === 2); + }); +}); diff --git a/app/mm-redux/actions/teams.ts b/app/mm-redux/actions/teams.ts new file mode 100644 index 000000000..01515b7d8 --- /dev/null +++ b/app/mm-redux/actions/teams.ts @@ -0,0 +1,735 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Client4} from '@mm-redux/client'; +import {General} from '../constants'; +import {ChannelTypes, TeamTypes, UserTypes} from '@mm-redux/action_types'; +import EventEmitter from '@mm-redux/utils/event_emitter'; + +import {isCompatibleWithJoinViewTeamPermissions} from '@mm-redux/selectors/entities/general'; + +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; + +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; + +import {GetStateFunc, DispatchFunc, ActionFunc, ActionResult, batchActions, Action} from '@mm-redux/types/actions'; + +import {Team, TeamMembership, TeamMemberWithError} from '@mm-redux/types/teams'; + +import {selectChannel} from './channels'; +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +import {getProfilesByIds, getStatusesByIds} from './users'; +import {loadRolesIfNeeded} from './roles'; +import {UserProfile} from '@mm-redux/types/users'; + +async function getProfilesAndStatusesForMembers(userIds: string[], dispatch: DispatchFunc, getState: GetStateFunc) { + const { + currentUserId, + profiles, + statuses, + } = getState().entities.users; + const profilesToLoad: string[] = []; + const statusesToLoad: string[] = []; + userIds.forEach((userId) => { + if (!profiles[userId] && !profilesToLoad.includes(userId) && userId !== currentUserId) { + profilesToLoad.push(userId); + } + + if (!statuses[userId] && !statusesToLoad.includes(userId) && userId !== currentUserId) { + statusesToLoad.push(userId); + } + }); + const requests: Promise[] = []; + + if (profilesToLoad.length) { + requests.push(dispatch(getProfilesByIds(profilesToLoad))); + } + + if (statusesToLoad.length) { + requests.push(dispatch(getStatusesByIds(statusesToLoad))); + } + + await Promise.all(requests); +} + +export function selectTeam(team: Team | string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const teamId = (typeof team === 'string') ? team : team.id; + dispatch({ + type: TeamTypes.SELECT_TEAM, + data: teamId, + }); + + return {data: true}; + }; +} + +export function getMyTeams(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getMyTeams, + onRequest: TeamTypes.MY_TEAMS_REQUEST, + onSuccess: [TeamTypes.RECEIVED_TEAMS_LIST, TeamTypes.MY_TEAMS_SUCCESS], + onFailure: TeamTypes.MY_TEAMS_FAILURE, + }); +} + +export function getMyTeamUnreads(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getMyTeamUnreads, + onSuccess: TeamTypes.RECEIVED_MY_TEAM_UNREADS, + }); +} + +export function getTeam(teamId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeam, + onSuccess: TeamTypes.RECEIVED_TEAM, + params: [ + teamId, + ], + }); +} + +export function getTeamByName(teamName: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeamByName, + onSuccess: TeamTypes.RECEIVED_TEAM, + params: [ + teamName, + ], + }); +} + +export function getTeams(page = 0, perPage: number = General.TEAMS_CHUNK_SIZE, includeTotalCount = false): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + + dispatch({type: TeamTypes.GET_TEAMS_REQUEST, data}); + + try { + data = await Client4.getTeams(page, perPage, includeTotalCount); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch({type: TeamTypes.GET_TEAMS_FAILURE, data}); + dispatch(logError(error)); + return {error}; + } + + const actions: Action[] = [ + { + type: TeamTypes.RECEIVED_TEAMS_LIST, + data: includeTotalCount ? data.teams : data, + }, + { + type: TeamTypes.GET_TEAMS_SUCCESS, + data, + }, + ]; + + if (includeTotalCount) { + actions.push({ + type: TeamTypes.RECEIVED_TOTAL_TEAM_COUNT, + data: data.total_count, + }); + } + + dispatch(batchActions(actions)); + + return {data}; + }; +} + +export function searchTeams(term: string, page?: number, perPage?: number): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: TeamTypes.GET_TEAMS_REQUEST, data: null}); + + let response; + try { + response = await Client4.searchTeams(term, page, perPage); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: TeamTypes.GET_TEAMS_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + const teams = response.teams || response; + + dispatch(batchActions([ + { + type: TeamTypes.RECEIVED_TEAMS_LIST, + data: teams, + }, + { + type: TeamTypes.GET_TEAMS_SUCCESS, + }, + ])); + + return {data: response}; + }; +} + +export function createTeam(team: Team): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let created; + try { + created = await Client4.createTeam(team); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const member = { + team_id: created.id, + user_id: getState().entities.users.currentUserId, + roles: `${General.TEAM_ADMIN_ROLE} ${General.TEAM_USER_ROLE}`, + delete_at: 0, + msg_count: 0, + mention_count: 0, + }; + + dispatch(batchActions([ + { + type: TeamTypes.CREATED_TEAM, + data: created, + }, + { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, + data: member, + }, + { + type: TeamTypes.SELECT_TEAM, + data: created.id, + }, + ])); + dispatch(loadRolesIfNeeded(member.roles.split(' '))); + + return {data: created}; + }; +} + +export function deleteTeam(teamId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.deleteTeam(teamId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const entities = getState().entities; + const { + currentTeamId, + } = entities.teams; + const actions: Action[] = []; + if (teamId === currentTeamId) { + EventEmitter.emit('leave_team'); + actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''}); + } + + actions.push( + { + type: TeamTypes.RECEIVED_TEAM_DELETED, + data: {id: teamId}, + } + ); + + dispatch(batchActions(actions)); + + return {data: true}; + }; +} + +export function updateTeam(team: Team): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.updateTeam, + onSuccess: TeamTypes.UPDATED_TEAM, + params: [ + team, + ], + }); +} + +export function patchTeam(team: Team): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.patchTeam, + onSuccess: TeamTypes.PATCHED_TEAM, + params: [ + team, + ], + }); +} + +export function regenerateTeamInviteId(teamId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.regenerateTeamInviteId, + onSuccess: TeamTypes.REGENERATED_TEAM_INVITE_ID, + params: [ + teamId, + ], + }); +} + +export function getMyTeamMembers(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const getMyTeamMembersFunc = bindClientFunc({ + clientFunc: Client4.getMyTeamMembers, + onSuccess: TeamTypes.RECEIVED_MY_TEAM_MEMBERS, + }); + const teamMembers = (await getMyTeamMembersFunc(dispatch, getState)) as ActionResult; + + if ('data' in teamMembers && teamMembers.data) { + const roles = new Set(); + + for (const teamMember of teamMembers.data) { + for (const role of teamMember.roles.split(' ')) { + roles.add(role); + } + } + if (roles.size > 0) { + dispatch(loadRolesIfNeeded([...roles])); + } + } + + return teamMembers; + }; +} + +export function getTeamMembers(teamId: string, page = 0, perPage: number = General.TEAMS_CHUNK_SIZE): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeamMembers, + onRequest: TeamTypes.GET_TEAM_MEMBERS_REQUEST, + onSuccess: [TeamTypes.RECEIVED_MEMBERS_IN_TEAM, TeamTypes.GET_TEAM_MEMBERS_SUCCESS], + onFailure: TeamTypes.GET_TEAM_MEMBERS_FAILURE, + params: [ + teamId, + page, + perPage, + ], + }); +} + +export function getTeamMember(teamId: string, userId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let member; + try { + const memberRequest = Client4.getTeamMember(teamId, userId); + + getProfilesAndStatusesForMembers([userId], dispatch, getState); + + member = await memberRequest; + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: TeamTypes.RECEIVED_MEMBERS_IN_TEAM, + data: [member], + }); + + return {data: member}; + }; +} + +export function getTeamMembersByIds(teamId: string, userIds: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let members; + try { + const membersRequest = Client4.getTeamMembersByIds(teamId, userIds); + + getProfilesAndStatusesForMembers(userIds, dispatch, getState); + + members = await membersRequest; + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: TeamTypes.RECEIVED_MEMBERS_IN_TEAM, + data: members, + }); + + return {data: members}; + }; +} + +export function getTeamsForUser(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeamsForUser, + onRequest: TeamTypes.GET_TEAMS_REQUEST, + onSuccess: [TeamTypes.RECEIVED_TEAMS_LIST, TeamTypes.GET_TEAMS_SUCCESS], + onFailure: TeamTypes.GET_TEAMS_FAILURE, + params: [ + userId, + ], + }); +} + +export function getTeamMembersForUser(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeamMembersForUser, + onSuccess: TeamTypes.RECEIVED_TEAM_MEMBERS, + params: [ + userId, + ], + }); +} + +export function getTeamStats(teamId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeamStats, + onSuccess: TeamTypes.RECEIVED_TEAM_STATS, + params: [ + teamId, + ], + }); +} + +export function addUserToTeamFromInvite(token: string, inviteId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.addToTeamFromInvite, + onRequest: TeamTypes.ADD_TO_TEAM_FROM_INVITE_REQUEST, + onSuccess: TeamTypes.ADD_TO_TEAM_FROM_INVITE_SUCCESS, + onFailure: TeamTypes.ADD_TO_TEAM_FROM_INVITE_FAILURE, + params: [ + token, + inviteId, + ], + }); +} + +export function addUserToTeam(teamId: string, userId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let member; + try { + member = await Client4.addToTeam(teamId, userId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: UserTypes.RECEIVED_PROFILE_IN_TEAM, + data: {id: teamId, user_id: userId}, + }, + { + type: TeamTypes.RECEIVED_MEMBER_IN_TEAM, + data: member, + }, + ])); + + return {data: member}; + }; +} + +export function addUsersToTeam(teamId: string, userIds: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let members; + try { + members = await Client4.addUsersToTeam(teamId, userIds); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const profiles: Partial[] = []; + members.forEach((m: TeamMembership) => profiles.push({id: m.user_id})); + + dispatch(batchActions([ + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM, + data: profiles, + id: teamId, + }, + { + type: TeamTypes.RECEIVED_MEMBERS_IN_TEAM, + data: members, + }, + ])); + + return {data: members}; + }; +} + +export function addUsersToTeamGracefully(teamId: string, userIds: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let result: Array; + try { + result = await Client4.addUsersToTeamGracefully(teamId, userIds); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const added_members = result ? result.filter((m) => !m.error) : []; + const profiles: Partial[] = added_members.map((m) => ({id: m.user_id})); + const members = added_members.map((m) => m.member); + dispatch(batchActions([ + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM, + data: profiles, + id: teamId, + }, + { + type: TeamTypes.RECEIVED_MEMBERS_IN_TEAM, + data: members, + }, + ])); + + return {data: result}; + }; +} + +export function removeUserFromTeam(teamId: string, userId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.removeFromTeam(teamId, userId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const member = { + team_id: teamId, + user_id: userId, + }; + + const actions: Action[] = [ + { + type: UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM, + data: {id: teamId, user_id: userId}, + }, + { + type: TeamTypes.REMOVE_MEMBER_FROM_TEAM, + data: member, + }, + ]; + + const state = getState(); + const currentUserId = getCurrentUserId(state); + + if (userId === currentUserId) { + const {channels, myMembers} = state.entities.channels; + + for (const channelMember of Object.values(myMembers)) { + const channel = channels[channelMember.channel_id]; + + if (channel && channel.team_id === teamId) { + actions.push({ + type: ChannelTypes.LEAVE_CHANNEL, + data: channel, + }); + } + } + + if (teamId === getCurrentTeamId(state)) { + actions.push(selectChannel('')); + } + } + + dispatch(batchActions(actions)); + + return {data: true}; + }; +} + +export function updateTeamMemberRoles(teamId: string, userId: string, roles: string[]): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateTeamMemberRoles(teamId, userId, roles); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const membersInTeam = getState().entities.teams.membersInTeam[teamId]; + if (membersInTeam && membersInTeam[userId]) { + dispatch({ + type: TeamTypes.RECEIVED_MEMBER_IN_TEAM, + data: {...membersInTeam[userId], roles}, + }); + } + + return {data: true}; + }; +} + +export function sendEmailInvitesToTeam(teamId: string, emails: Array): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.sendEmailInvitesToTeam, + params: [ + teamId, + emails, + ], + }); +} + +export function sendEmailGuestInvitesToChannels(teamId: string, channelIds: Array, emails: Array, message: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.sendEmailGuestInvitesToChannels, + params: [ + teamId, + channelIds, + emails, + message, + ], + }); +} +export function sendEmailInvitesToTeamGracefully(teamId: string, emails: Array): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.sendEmailInvitesToTeamGracefully, + params: [ + teamId, + emails, + ], + }); +} + +export function sendEmailGuestInvitesToChannelsGracefully(teamId: string, channelIds: Array, emails: Array, message: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.sendEmailGuestInvitesToChannelsGracefully, + params: [ + teamId, + channelIds, + emails, + message, + ], + }); +} + +export function getTeamInviteInfo(inviteId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTeamInviteInfo, + onRequest: TeamTypes.TEAM_INVITE_INFO_REQUEST, + onSuccess: TeamTypes.TEAM_INVITE_INFO_SUCCESS, + onFailure: TeamTypes.TEAM_INVITE_INFO_FAILURE, + params: [ + inviteId, + ], + }); +} + +export function checkIfTeamExists(teamName: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.checkIfTeamExists(teamName); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + return {data: data.exists}; + }; +} + +export function joinTeam(inviteId: string, teamId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: TeamTypes.JOIN_TEAM_REQUEST, data: null}); + + const state = getState(); + try { + if (isCompatibleWithJoinViewTeamPermissions(state)) { + const currentUserId = state.entities.users.currentUserId; + await Client4.addToTeam(teamId, currentUserId); + } else { + await Client4.joinTeam(inviteId); + } + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: TeamTypes.JOIN_TEAM_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + getMyTeamUnreads()(dispatch, getState); + + await Promise.all([ + getTeam(teamId)(dispatch, getState), + getMyTeamMembers()(dispatch, getState), + ]); + + dispatch({type: TeamTypes.JOIN_TEAM_SUCCESS, data: null}); + return {data: true}; + }; +} + +export function setTeamIcon(teamId: string, imageData: any): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.setTeamIcon, + params: [ + teamId, + imageData, + ], + }); +} + +export function removeTeamIcon(teamId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.removeTeamIcon, + params: [ + teamId, + ], + }); +} + +export function updateTeamScheme(teamId: string, schemeId: string): ActionFunc { + return bindClientFunc({ + clientFunc: async () => { + await Client4.updateTeamScheme(teamId, schemeId); + return {teamId, schemeId}; + }, + onSuccess: TeamTypes.UPDATED_TEAM_SCHEME, + }); +} + +export function updateTeamMemberSchemeRoles( + teamId: string, + userId: string, + isSchemeUser: boolean, + isSchemeAdmin: boolean +): ActionFunc { + return bindClientFunc({ + clientFunc: async () => { + await Client4.updateTeamMemberSchemeRoles(teamId, userId, isSchemeUser, isSchemeAdmin); + return {teamId, userId, isSchemeUser, isSchemeAdmin}; + }, + onSuccess: TeamTypes.UPDATED_TEAM_MEMBER_SCHEME_ROLES, + }); +} + +export function invalidateAllEmailInvites(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.invalidateAllEmailInvites, + }); +} + +export function membersMinusGroupMembers(teamID: string, groupIDs: Array, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.teamMembersMinusGroupMembers, + onSuccess: TeamTypes.RECEIVED_TEAM_MEMBERS_MINUS_GROUP_MEMBERS, + params: [ + teamID, + groupIDs, + page, + perPage, + ], + }); +} diff --git a/app/mm-redux/actions/timezone.ts b/app/mm-redux/actions/timezone.ts new file mode 100644 index 000000000..e48d93d4f --- /dev/null +++ b/app/mm-redux/actions/timezone.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {getUserTimezone} from '@mm-redux/selectors/entities/timezone'; +import {updateMe} from './users'; +import {DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; +export function autoUpdateTimezone(deviceTimezone: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const currentUer = getCurrentUser(getState()); + const currentTimezone = getUserTimezone(getState(), currentUer.id); + const newTimezoneExists = currentTimezone.automaticTimezone !== deviceTimezone; + + if (currentTimezone.useAutomaticTimezone && newTimezoneExists) { + const timezone = { + useAutomaticTimezone: 'true', + automaticTimezone: deviceTimezone, + manualTimezone: currentTimezone.manualTimezone, + }; + + const updatedUser = { + ...currentUer, + timezone, + }; + + updateMe(updatedUser)(dispatch, getState); + } + }; +} diff --git a/app/mm-redux/actions/users.test.js b/app/mm-redux/actions/users.test.js new file mode 100644 index 000000000..d524337b4 --- /dev/null +++ b/app/mm-redux/actions/users.test.js @@ -0,0 +1,1376 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; +import fs from 'fs'; + +import * as Actions from '@mm-redux/actions/users'; +import {Client4} from '@mm-redux/client'; +import {RequestStatus} from '../constants'; +import TestHelper from 'test/test_helper'; +import configureStore from 'test/test_store'; +import deepFreeze from '@mm-redux/utils/deep_freeze'; + +const OK_RESPONSE = {status: 'OK'}; + +describe('Actions.Users', () => { + let store; + beforeAll(async () => { + await TestHelper.initBasic(Client4); + }); + + beforeEach(async () => { + store = await configureStore(); + }); + + afterAll(async () => { + await TestHelper.tearDown(); + }); + + it('createUser', async () => { + const userToCreate = TestHelper.fakeUser(); + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, {...userToCreate, id: TestHelper.generateId()}); + + const {data: user} = await Actions.createUser(userToCreate)(store.dispatch, store.getState); + + const state = store.getState(); + const {profiles} = state.entities.users; + + assert.ok(profiles); + assert.ok(profiles[user.id]); + }); + + it('login', async () => { + const user = TestHelper.basicUser; + + nock(Client4.getBaseRoute()). + post('/users/logout'). + reply(200, OK_RESPONSE); + + await TestHelper.basicClient4.logout(); + + TestHelper.mockLogin(); + + await Actions.login(user.email, user.password)(store.dispatch, store.getState); + + const state = store.getState(); + const loginRequest = state.requests.users.login; + const {currentUserId, profiles} = state.entities.users; + const preferences = state.entities.preferences.myPreferences; + const teamMembers = state.entities.teams.myMembers; + const serverVersion = state.entities.general.serverVersion; + + if (loginRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(loginRequest.error)); + } + + assert.ok(currentUserId); + assert.ok(profiles); + assert.ok(profiles[currentUserId]); + assert.ok(Object.keys(preferences).length); + assert.ok(serverVersion); + + Object.keys(teamMembers).forEach((id) => { + assert.ok(teamMembers[id].team_id); + assert.equal(teamMembers[id].user_id, currentUserId); + }); + }); + + it('loginById', async () => { + const user = TestHelper.basicUser; + + nock(Client4.getBaseRoute()). + post('/users/logout'). + reply(200, OK_RESPONSE); + + await TestHelper.basicClient4.logout(); + + TestHelper.mockLogin(); + + await Actions.loginById(user.id, 'password1')(store.dispatch, store.getState); + + const state = store.getState(); + const {currentUserId, profiles} = state.entities.users; + const preferences = state.entities.preferences.myPreferences; + const teamMembers = state.entities.teams.myMembers; + + assert.ok(currentUserId); + assert.ok(profiles); + assert.ok(profiles[currentUserId]); + assert.ok(Object.keys(preferences).length); + + Object.keys(teamMembers).forEach((id) => { + assert.ok(teamMembers[id].team_id); + assert.equal(teamMembers[id].user_id, currentUserId); + }); + }); + + it('getTermsOfService', async () => { + const response = { + create_at: 1537976679426, + id: '1234', + text: 'Terms of Service', + user_id: '1', + }; + + nock(Client4.getBaseRoute()). + get('/terms_of_service'). + reply(200, response); + + const {data} = await Actions.getTermsOfService()(store.dispatch, store.getState); + + assert.deepEqual(data, response); + }); + + it('updateMyTermsOfServiceStatus accept terms', async () => { + const user = TestHelper.basicUser; + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, {...TestHelper.fakeUserWithId()}); + + TestHelper.mockLogin(); + await Actions.login(user.email, 'password1')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post('/users/me/terms_of_service'). + reply(200, OK_RESPONSE); + + await Actions.updateMyTermsOfServiceStatus(1, true)(store.dispatch, store.getState); + + const {currentUserId} = store.getState().entities.users; + const currentUser = store.getState().entities.users.profiles[currentUserId]; + + assert.ok(currentUserId); + assert.ok(currentUser.terms_of_service_id); + assert.ok(currentUser.terms_of_service_create_at); + assert.equal(currentUser.terms_of_service_id, 1); + }); + + it('updateMyTermsOfServiceStatus reject terms', async () => { + const user = TestHelper.basicUser; + nock(Client4.getBaseRoute()). + post('/users'). + reply(201, {...TestHelper.fakeUserWithId()}); + + TestHelper.mockLogin(); + await Actions.login(user.email, 'password1')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post('/users/me/terms_of_service'). + reply(200, OK_RESPONSE); + + await Actions.updateMyTermsOfServiceStatus(1, false)(store.dispatch, store.getState); + + const {currentUserId, myAcceptedTermsOfServiceId} = store.getState().entities.users; + + assert.ok(currentUserId); + assert.notEqual(myAcceptedTermsOfServiceId, 1); + }); + + it('logout', async () => { + nock(Client4.getBaseRoute()). + post('/users/logout'). + reply(200, OK_RESPONSE); + + await Actions.logout()(store.dispatch, store.getState); + + const state = store.getState(); + const logoutRequest = state.requests.users.logout; + const general = state.entities.general; + const users = state.entities.users; + const teams = state.entities.teams; + const channels = state.entities.channels; + const posts = state.entities.posts; + const preferences = state.entities.preferences; + + if (logoutRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(logoutRequest.error)); + } + + assert.deepStrictEqual(general.config, {}, 'config not empty'); + assert.deepStrictEqual(general.license, {}, 'license not empty'); + assert.strictEqual(users.currentUserId, '', 'current user id not empty'); + assert.deepStrictEqual(users.mySessions, [], 'user sessions not empty'); + assert.deepStrictEqual(users.myAudits, [], 'user audits not empty'); + assert.deepStrictEqual(users.profiles, {}, 'user profiles not empty'); + assert.deepStrictEqual(users.profilesInTeam, {}, 'users profiles in team not empty'); + assert.deepStrictEqual(users.profilesInChannel, {}, 'users profiles in channel not empty'); + assert.deepStrictEqual(users.profilesNotInChannel, {}, 'users profiles NOT in channel not empty'); + assert.deepStrictEqual(users.statuses, {}, 'users statuses not empty'); + assert.strictEqual(teams.currentTeamId, '', 'current team id is not empty'); + assert.deepStrictEqual(teams.teams, {}, 'teams is not empty'); + assert.deepStrictEqual(teams.myMembers, {}, 'team members is not empty'); + assert.deepStrictEqual(teams.membersInTeam, {}, 'members in team is not empty'); + assert.deepStrictEqual(teams.stats, {}, 'team stats is not empty'); + assert.strictEqual(channels.currentChannelId, '', 'current channel id is not empty'); + assert.deepStrictEqual(channels.channels, {}, 'channels is not empty'); + assert.deepStrictEqual(channels.channelsInTeam, {}, 'channelsInTeam is not empty'); + assert.deepStrictEqual(channels.myMembers, {}, 'channel members is not empty'); + assert.deepStrictEqual(channels.stats, {}, 'channel stats is not empty'); + assert.strictEqual(posts.selectedPostId, '', 'selected post id is not empty'); + assert.strictEqual(posts.currentFocusedPostId, '', 'current focused post id is not empty'); + assert.deepStrictEqual(posts.posts, {}, 'posts is not empty'); + assert.deepStrictEqual(posts.postsInChannel, {}, 'posts by channel is not empty'); + assert.deepStrictEqual(preferences.myPreferences, {}, 'user preferences not empty'); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, TestHelper.basicUser); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + }); + + it('getProfiles', async () => { + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(200, [TestHelper.basicUser]); + + await Actions.getProfiles(0)(store.dispatch, store.getState); + const {profiles} = store.getState().entities.users; + + assert.ok(Object.keys(profiles).length); + }); + + it('getProfilesByIds', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + post('/users/ids'). + reply(200, [user]); + + await Actions.getProfilesByIds([user.id])(store.dispatch, store.getState); + const {profiles} = store.getState().entities.users; + + assert.ok(profiles[user.id]); + }); + + it('getMissingProfilesByIds', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + post('/users/ids'). + reply(200, [user]); + + await Actions.getMissingProfilesByIds([user.id])(store.dispatch, store.getState); + const {profiles} = store.getState().entities.users; + + assert.ok(profiles[user.id]); + }); + + it('getProfilesByUsernames', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + post('/users/usernames'). + reply(200, [user]); + + await Actions.getProfilesByUsernames([user.username])(store.dispatch, store.getState); + const {profiles} = store.getState().entities.users; + + assert.ok(profiles[user.id]); + }); + + it('getProfilesInTeam', async () => { + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(200, [TestHelper.basicUser]); + + await Actions.getProfilesInTeam(TestHelper.basicTeam.id, 0)(store.dispatch, store.getState); + + const {profilesInTeam, profiles} = store.getState().entities.users; + const team = profilesInTeam[TestHelper.basicTeam.id]; + + assert.ok(team); + assert.ok(team.has(TestHelper.basicUser.id)); + assert.equal(Object.keys(profiles).length, team.size, 'profiles != profiles in team'); + }); + + it('getProfilesNotInTeam', async () => { + const team = TestHelper.basicTeam; + + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(200, [user]); + + await Actions.getProfilesNotInTeam(team.id, 0)(store.dispatch, store.getState); + + const {profilesNotInTeam} = store.getState().entities.users; + const notInTeam = profilesNotInTeam[team.id]; + + assert.ok(notInTeam); + assert.ok(notInTeam.size > 0); + }); + + it('getProfilesWithoutTeam', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(200, [user]); + + await Actions.getProfilesWithoutTeam(0)(store.dispatch, store.getState); + const {profilesWithoutTeam, profiles} = store.getState().entities.users; + + assert.ok(profilesWithoutTeam); + assert.ok(profilesWithoutTeam.size > 0); + assert.ok(profiles); + assert.ok(Object.keys(profiles).length > 0); + }); + + it('getProfilesInChannel', async () => { + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(200, [TestHelper.basicUser]); + + await Actions.getProfilesInChannel( + TestHelper.basicChannel.id, + 0, + )(store.dispatch, store.getState); + + const {profiles, profilesInChannel} = store.getState().entities.users; + + const channel = profilesInChannel[TestHelper.basicChannel.id]; + assert.ok(channel.has(TestHelper.basicUser.id)); + assert.equal(Object.keys(profiles).length, channel.size, 'profiles != profiles in channel'); + }); + + it('getProfilesNotInChannel', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(200, [user]); + + await Actions.getProfilesNotInChannel( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id, + 0, + )(store.dispatch, store.getState); + + const {profiles, profilesNotInChannel} = store.getState().entities.users; + + const channel = profilesNotInChannel[TestHelper.basicChannel.id]; + assert.ok(channel.has(user.id)); + assert.equal(Object.keys(profiles).length, channel.size, 'profiles != profiles in channel'); + }); + + it('getUser', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + get(`/users/${user.id}`). + reply(200, user); + + await Actions.getUser( + user.id, + )(store.dispatch, store.getState); + + const state = store.getState(); + const {profiles} = state.entities.users; + + assert.ok(profiles[user.id]); + assert.equal(profiles[user.id].id, user.id); + }); + + it('getMe', async () => { + nock(Client4.getBaseRoute()). + get('/users/me'). + reply(200, TestHelper.basicUser); + + await Actions.getMe()(store.dispatch, store.getState); + + const state = store.getState(); + const {profiles, currentUserId} = state.entities.users; + + assert.ok(profiles[currentUserId]); + assert.equal(profiles[currentUserId].id, currentUserId); + }); + + it('getUserByUsername', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + get(`/users/username/${user.username}`). + reply(200, user); + + await Actions.getUserByUsername( + user.username, + )(store.dispatch, store.getState); + + const state = store.getState(); + const {profiles} = state.entities.users; + + assert.ok(profiles[user.id]); + assert.equal(profiles[user.id].username, user.username); + }); + + it('getUserByEmail', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser(TestHelper.fakeUser()); + + nock(Client4.getBaseRoute()). + get(`/users/email/${user.email}`). + reply(200, user); + + await Actions.getUserByEmail( + user.email, + )(store.dispatch, store.getState); + + const state = store.getState(); + const {profiles} = state.entities.users; + + assert.ok(profiles[user.id]); + assert.equal(profiles[user.id].email, user.email); + }); + + it('searchProfiles', async () => { + const user = TestHelper.basicUser; + + nock(Client4.getBaseRoute()). + post('/users/search'). + reply(200, [user]); + + await Actions.searchProfiles( + user.username, + )(store.dispatch, store.getState); + + const state = store.getState(); + const {profiles} = state.entities.users; + + assert.ok(profiles[user.id]); + assert.equal(profiles[user.id].id, user.id); + }); + + it('getStatusesByIds', async () => { + nock(Client4.getBaseRoute()). + post('/users/status/ids'). + reply(200, [{user_id: TestHelper.basicUser.id, status: 'online', manual: false, last_activity_at: 1507662212199}]); + + await Actions.getStatusesByIds( + [TestHelper.basicUser.id], + )(store.dispatch, store.getState); + + const statuses = store.getState().entities.users.statuses; + + assert.ok(statuses[TestHelper.basicUser.id]); + assert.equal(Object.keys(statuses).length, 1); + }); + + it('getTotalUsersStats', async () => { + nock(Client4.getBaseRoute(TestHelper.basicUser.id)). + get('/users/stats'). + reply(200, {total_users_count: 2605}); + await Actions.getTotalUsersStats()(store.dispatch, store.getState); + + const {stats} = store.getState().entities.users; + + assert.equal(stats.total_users_count, 2605); + }); + + it('getStatus', async () => { + const user = TestHelper.basicUser; + + nock(Client4.getBaseRoute()). + get(`/users/${user.id}/status`). + reply(200, {user_id: user.id, status: 'online', manual: false, last_activity_at: 1507662212199}); + + await Actions.getStatus( + user.id, + )(store.dispatch, store.getState); + + const statuses = store.getState().entities.users.statuses; + assert.ok(statuses[user.id]); + }); + + it('setStatus', async () => { + nock(Client4.getBaseRoute()). + put(`/users/${TestHelper.basicUser.id}/status`). + reply(200, OK_RESPONSE); + + await Actions.setStatus( + {user_id: TestHelper.basicUser.id, status: 'away'}, + )(store.dispatch, store.getState); + + const statuses = store.getState().entities.users.statuses; + assert.ok(statuses[TestHelper.basicUser.id] === 'away'); + }); + + it('getSessions', async () => { + nock(Client4.getBaseRoute()). + get(`/users/${TestHelper.basicUser.id}/sessions`). + reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]); + + await Actions.getSessions(TestHelper.basicUser.id)(store.dispatch, store.getState); + + const sessions = store.getState().entities.users.mySessions; + + assert.ok(sessions.length); + assert.equal(sessions[0].user_id, TestHelper.basicUser.id); + }); + + it('revokeSession', async () => { + nock(Client4.getBaseRoute()). + get(`/users/${TestHelper.basicUser.id}/sessions`). + reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]); + + await Actions.getSessions(TestHelper.basicUser.id)(store.dispatch, store.getState); + + let sessions = store.getState().entities.users.mySessions; + + const sessionsLength = sessions.length; + + nock(Client4.getBaseRoute()). + post(`/users/${TestHelper.basicUser.id}/sessions/revoke`). + reply(200, OK_RESPONSE); + await Actions.revokeSession(TestHelper.basicUser.id, sessions[0].id)(store.dispatch, store.getState); + + sessions = store.getState().entities.users.mySessions; + assert.ok(sessions.length === sessionsLength - 1); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, TestHelper.basicUser); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + }); + + it('revokeSession and logout', async () => { + nock(Client4.getBaseRoute()). + get(`/users/${TestHelper.basicUser.id}/sessions`). + reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]); + + await Actions.getSessions(TestHelper.basicUser.id)(store.dispatch, store.getState); + + const sessions = store.getState().entities.users.mySessions; + + nock(Client4.getBaseRoute()). + post(`/users/${TestHelper.basicUser.id}/sessions/revoke`). + reply(200, OK_RESPONSE); + + const {data: revokeSessionResponse} = await Actions.revokeSession(TestHelper.basicUser.id, sessions[0].id)(store.dispatch, store.getState); + assert.deepEqual(revokeSessionResponse, true); + + nock(Client4.getBaseRoute()). + get('/users'). + reply(401, {}); + + await Actions.getProfiles(0)(store.dispatch, store.getState); + + const basicUser = TestHelper.basicUser; + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, basicUser); + const response = await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + assert.deepEqual(response.email, basicUser.email); + }); + + it('revokeAllSessionsForCurrentUser', async () => { + const user = TestHelper.basicUser; + nock(Client4.getBaseRoute()). + post('/users/logout'). + reply(200, OK_RESPONSE); + await TestHelper.basicClient4.logout(); + let sessions = store.getState().entities.users.mySessions; + + assert.strictEqual(sessions.length, 0); + + TestHelper.mockLogin(); + await Actions.loginById(user.id, 'password1')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, TestHelper.basicUser); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + + nock(Client4.getBaseRoute()). + get(`/users/${user.id}/sessions`). + reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}, {id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]); + await Actions.getSessions(user.id)(store.dispatch, store.getState); + + sessions = store.getState().entities.users.mySessions; + assert.ok(sessions.length > 1); + + nock(Client4.getBaseRoute()). + post(`/users/${user.id}/sessions/revoke/all`). + reply(200, OK_RESPONSE); + const {data} = await Actions.revokeAllSessionsForUser(user.id)(store.dispatch, store.getState); + assert.deepEqual(data, true); + + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(401, {}); + await Actions.getProfiles(0)(store.dispatch, store.getState); + + const logoutRequest = store.getState().requests.users.logout; + if (logoutRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(logoutRequest.error)); + } + + sessions = store.getState().entities.users.mySessions; + + assert.strictEqual(sessions.length, 0); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, TestHelper.basicUser); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + }); + + it('revokeSessionsForAllUsers', async () => { + const user = TestHelper.basicUser; + nock(Client4.getBaseRoute()). + post('/users/logout'). + reply(200, OK_RESPONSE); + await TestHelper.basicClient4.logout(); + let sessions = store.getState().entities.users.mySessions; + + assert.strictEqual(sessions.length, 0); + + TestHelper.mockLogin(); + await Actions.loginById(user.id, 'password1')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, TestHelper.basicUser); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + + nock(Client4.getBaseRoute()). + get(`/users/${user.id}/sessions`). + reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}, {id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]); + await Actions.getSessions(user.id)(store.dispatch, store.getState); + + sessions = store.getState().entities.users.mySessions; + assert.ok(sessions.length > 1); + + nock(Client4.getBaseRoute()). + post('/users/sessions/revoke/all'). + reply(200, OK_RESPONSE); + const {data} = await Actions.revokeSessionsForAllUsers(user.id)(store.dispatch, store.getState); + assert.deepEqual(data, true); + + nock(Client4.getBaseRoute()). + get('/users'). + query(true). + reply(401, {}); + await Actions.getProfiles(0)(store.dispatch, store.getState); + + const logoutRequest = store.getState().requests.users.logout; + if (logoutRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(logoutRequest.error)); + } + + sessions = store.getState().entities.users.mySessions; + + assert.strictEqual(sessions.length, 0); + + nock(Client4.getBaseRoute()). + post('/users/login'). + reply(200, TestHelper.basicUser); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + }); + + it('getUserAudits', async () => { + nock(Client4.getBaseRoute()). + get(`/users/${TestHelper.basicUser.id}/audits`). + query(true). + reply(200, [{id: TestHelper.generateId(), create_at: 1497285546645, user_id: TestHelper.basicUser.id, action: '/api/v4/users/login', extra_info: 'success', ip_address: '::1', session_id: ''}]); + + await Actions.getUserAudits(TestHelper.basicUser.id)(store.dispatch, store.getState); + + const audits = store.getState().entities.users.myAudits; + + assert.ok(audits.length); + assert.equal(audits[0].user_id, TestHelper.basicUser.id); + }); + + it('autocompleteUsers', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + query(true). + reply(200, TestHelper.fakeUserWithId()); + + const user = await TestHelper.basicClient4.createUser( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id, + ); + + nock(Client4.getBaseRoute()). + get('/users/autocomplete'). + query(true). + reply(200, {users: [TestHelper.basicUser], out_of_channel: [user]}); + + await Actions.autocompleteUsers( + '', + TestHelper.basicTeam.id, + TestHelper.basicChannel.id, + )(store.dispatch, store.getState); + + const autocompleteRequest = store.getState().requests.users.autocompleteUsers; + const {profiles, profilesNotInChannel, profilesInChannel} = store.getState().entities.users; + + if (autocompleteRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(autocompleteRequest.error)); + } + + const notInChannel = profilesNotInChannel[TestHelper.basicChannel.id]; + const inChannel = profilesInChannel[TestHelper.basicChannel.id]; + assert.ok(notInChannel.has(user.id)); + assert.ok(inChannel.has(TestHelper.basicUser.id)); + assert.ok(profiles[user.id]); + }); + + it('updateMe', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, TestHelper.basicUser.password)(store.dispatch, store.getState); + + const state = store.getState(); + const currentUser = state.entities.users.profiles[state.entities.users.currentUserId]; + const notifyProps = currentUser.notify_props; + + nock(Client4.getBaseRoute()). + put('/users/me/patch'). + query(true). + reply(200, { + ...currentUser, + notify_props: { + ...notifyProps, + comments: 'any', + email: 'false', + first_name: 'false', + mention_keys: '', + user_id: currentUser.id, + }, + }); + + await Actions.updateMe({ + notify_props: { + ...notifyProps, + comments: 'any', + email: 'false', + first_name: 'false', + mention_keys: '', + user_id: currentUser.id, + }, + })(store.dispatch, store.getState); + + const updateRequest = store.getState().requests.users.updateMe; + const {currentUserId, profiles} = store.getState().entities.users; + const updateNotifyProps = profiles[currentUserId].notify_props; + + if (updateRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(updateRequest.error)); + } + + assert.equal(updateNotifyProps.comments, 'any'); + assert.equal(updateNotifyProps.email, 'false'); + assert.equal(updateNotifyProps.first_name, 'false'); + assert.equal(updateNotifyProps.mention_keys, ''); + }); + + it('patchUser', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, TestHelper.basicUser.password)(store.dispatch, store.getState); + + const state = store.getState(); + const currentUserId = state.entities.users.currentUserId; + const currentUser = state.entities.users.profiles[currentUserId]; + const notifyProps = currentUser.notify_props; + + nock(Client4.getBaseRoute()). + put(`/users/${currentUserId}/patch`). + query(true). + reply(200, { + ...currentUser, + notify_props: { + ...notifyProps, + comments: 'any', + email: 'false', + first_name: 'false', + mention_keys: '', + user_id: currentUser.id, + }, + }); + + await Actions.patchUser({ + id: currentUserId, + notify_props: { + ...notifyProps, + comments: 'any', + email: 'false', + first_name: 'false', + mention_keys: '', + user_id: currentUser.id, + }, + })(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + const updateNotifyProps = profiles[currentUserId].notify_props; + + assert.equal(updateNotifyProps.comments, 'any'); + assert.equal(updateNotifyProps.email, 'false'); + assert.equal(updateNotifyProps.first_name, 'false'); + assert.equal(updateNotifyProps.mention_keys, ''); + }); + + it('updateUserRoles', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + put(`/users/${currentUserId}/roles`). + reply(200, OK_RESPONSE); + + await Actions.updateUserRoles(currentUserId, 'system_user system_admin')(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + const currentUserRoles = profiles[currentUserId].roles; + + assert.equal(currentUserRoles, 'system_user system_admin'); + }); + + it('updateUserMfa', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + put(`/users/${currentUserId}/mfa`). + reply(200, OK_RESPONSE); + + await Actions.updateUserMfa(currentUserId, false, '')(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + const currentUserMfa = profiles[currentUserId].mfa_active; + + assert.equal(currentUserMfa, false); + }); + + it('updateUserPassword', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const beforeTime = new Date().getTime(); + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + put(`/users/${currentUserId}/password`). + reply(200, OK_RESPONSE); + + await Actions.updateUserPassword(currentUserId, 'password1', 'password1')(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + const currentUser = profiles[currentUserId]; + + assert.ok(currentUser); + assert.ok(currentUser.last_password_update_at > beforeTime); + }); + + it('checkMfa', async () => { + const user = TestHelper.basicUser; + + nock(Client4.getBaseRoute()). + post('/users/mfa'). + reply(200, {mfa_required: false}); + + const {data: mfaRequired} = await Actions.checkMfa(user.email)(store.dispatch, store.getState); + + const state = store.getState(); + const mfaRequest = state.requests.users.checkMfa; + + if (mfaRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(mfaRequest.error)); + } + + assert.ok(!mfaRequired); + }); + + it('generateMfaSecret', async () => { + const response = {secret: 'somesecret', qr_code: 'someqrcode'}; + + nock(Client4.getBaseRoute()). + post('/users/me/mfa/generate'). + reply(200, response); + + const {data} = await Actions.generateMfaSecret('me')(store.dispatch, store.getState); + + assert.deepEqual(data, response); + }); + + it('updateUserActive', async () => { + nock(Client4.getBaseRoute()). + post('/users'). + reply(200, TestHelper.fakeUserWithId()); + + const {data: user} = await Actions.createUser(TestHelper.fakeUser())(store.dispatch, store.getState); + + const beforeTime = new Date().getTime(); + + nock(Client4.getBaseRoute()). + put(`/users/${user.id}/active`). + reply(200, OK_RESPONSE); + await Actions.updateUserActive(user.id, false)(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + + assert.ok(profiles[user.id]); + assert.ok(profiles[user.id].delete_at > beforeTime); + }); + + it('verifyUserEmail', async () => { + nock(Client4.getBaseRoute()). + post('/users/email/verify'). + reply(200, OK_RESPONSE); + + const {data} = await Actions.verifyUserEmail('sometoken')(store.dispatch, store.getState); + + assert.deepEqual(data, OK_RESPONSE); + }); + + it('sendVerificationEmail', async () => { + nock(Client4.getBaseRoute()). + post('/users/email/verify/send'). + reply(200, OK_RESPONSE); + + const {data} = await Actions.sendVerificationEmail(TestHelper.basicUser.email)(store.dispatch, store.getState); + + assert.deepEqual(data, OK_RESPONSE); + }); + + it('resetUserPassword', async () => { + nock(Client4.getBaseRoute()). + post('/users/password/reset'). + reply(200, OK_RESPONSE); + + const {data} = await Actions.resetUserPassword('sometoken', 'newpassword')(store.dispatch, store.getState); + + assert.deepEqual(data, OK_RESPONSE); + }); + + it('sendPasswordResetEmail', async () => { + nock(Client4.getBaseRoute()). + post('/users/password/reset/send'). + reply(200, OK_RESPONSE); + + const {data} = await Actions.sendPasswordResetEmail(TestHelper.basicUser.email)(store.dispatch, store.getState); + + assert.deepEqual(data, OK_RESPONSE); + }); + + it('uploadProfileImage', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const testImageData = fs.createReadStream('test/assets/images/test.png'); + + const beforeTime = new Date().getTime(); + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${TestHelper.basicUser.id}/image`). + reply(200, OK_RESPONSE); + + await Actions.uploadProfileImage(currentUserId, testImageData)(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + const currentUser = profiles[currentUserId]; + + assert.ok(currentUser); + assert.ok(currentUser.last_picture_update > beforeTime); + }); + + it('setDefaultProfileImage', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + delete(`/users/${TestHelper.basicUser.id}/image`). + reply(200, OK_RESPONSE); + + await Actions.setDefaultProfileImage(currentUserId)(store.dispatch, store.getState); + + const {profiles} = store.getState().entities.users; + const currentUser = profiles[currentUserId]; + + assert.ok(currentUser); + assert.equal(currentUser.last_picture_update, 0); + }); + + it('switchEmailToOAuth', async () => { + nock(Client4.getBaseRoute()). + post('/users/login/switch'). + reply(200, {follow_link: '/login'}); + + const {data} = await Actions.switchEmailToOAuth('gitlab', TestHelper.basicUser.email, TestHelper.basicUser.password)(store.dispatch, store.getState); + assert.deepEqual(data, {follow_link: '/login'}); + }); + + it('switchOAuthToEmail', async () => { + nock(Client4.getBaseRoute()). + post('/users/login/switch'). + reply(200, {follow_link: '/login'}); + + const {data} = await Actions.switchOAuthToEmail('gitlab', TestHelper.basicUser.email, TestHelper.basicUser.password)(store.dispatch, store.getState); + + assert.deepEqual(data, {follow_link: '/login'}); + }); + + it('switchEmailToLdap', async () => { + nock(Client4.getBaseRoute()). + post('/users/login/switch'). + reply(200, {follow_link: '/login'}); + + const {data} = await Actions.switchEmailToLdap(TestHelper.basicUser.email, TestHelper.basicUser.password, 'someid', 'somepassword')(store.dispatch, store.getState); + + assert.deepEqual(data, {follow_link: '/login'}); + }); + + it('switchLdapToEmail', (done) => { + async function test() { + nock(Client4.getBaseRoute()). + post('/users/login/switch'). + reply(200, {follow_link: '/login'}); + + const {data} = await Actions.switchLdapToEmail('somepassword', TestHelper.basicUser.email, TestHelper.basicUser.password)(store.dispatch, store.getState); + assert.deepEqual(data, {follow_link: '/login'}); + + done(); + } + + test(); + }); + + it('createUserAccessToken', (done) => { + async function test() { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + + const {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[data.id]); + assert.ok(!myUserAccessTokens[data.id].token); + done(); + } + + test(); + }); + + it('getUserAccessToken', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/users/tokens/${data.id}`). + reply(200, {id: data.id, description: 'test token', user_id: currentUserId}); + + await Actions.getUserAccessToken(data.id)(store.dispatch, store.getState); + + const {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[data.id]); + assert.ok(!myUserAccessTokens[data.id].token); + }); + + it('getUserAccessTokens', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get('/users/tokens'). + query(true). + reply(200, [{id: data.id, description: 'test token', user_id: currentUserId}]); + + await Actions.getUserAccessTokens()(store.dispatch, store.getState); + + const {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[data.id]); + assert.ok(!myUserAccessTokens[data.id].token); + }); + + it('getUserAccessTokensForUser', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + + nock(Client4.getBaseRoute()). + get(`/users/${currentUserId}/tokens`). + query(true). + reply(200, [{id: data.id, description: 'test token', user_id: currentUserId}]); + + await Actions.getUserAccessTokensForUser(currentUserId)(store.dispatch, store.getState); + + const {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[data.id]); + assert.ok(!myUserAccessTokens[data.id].token); + }); + + it('revokeUserAccessToken', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + + let {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[data.id]); + assert.ok(!myUserAccessTokens[data.id].token); + + nock(Client4.getBaseRoute()). + post('/users/tokens/revoke'). + reply(200, OK_RESPONSE); + + await Actions.revokeUserAccessToken(data.id)(store.dispatch, store.getState); + + myUserAccessTokens = store.getState().entities.users.myUserAccessTokens; + + assert.ok(myUserAccessTokens); + assert.ok(!myUserAccessTokens[data.id]); + }); + + it('disableUserAccessToken', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + const testId = data.id; + + let {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[testId]); + assert.ok(!myUserAccessTokens[testId].token); + + nock(Client4.getBaseRoute()). + post('/users/tokens/disable'). + reply(200, OK_RESPONSE); + + await Actions.disableUserAccessToken(testId)(store.dispatch, store.getState); + + myUserAccessTokens = store.getState().entities.users.myUserAccessTokens; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[testId]); + assert.ok(!myUserAccessTokens[testId].is_active); + assert.ok(!myUserAccessTokens[testId].token); + }); + + it('enableUserAccessToken', async () => { + TestHelper.mockLogin(); + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + const {data} = await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + const testId = data.id; + + let {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[testId]); + assert.ok(!myUserAccessTokens[testId].token); + + nock(Client4.getBaseRoute()). + post('/users/tokens/enable'). + reply(200, OK_RESPONSE); + + await Actions.enableUserAccessToken(testId)(store.dispatch, store.getState); + + myUserAccessTokens = store.getState().entities.users.myUserAccessTokens; + + assert.ok(myUserAccessTokens); + assert.ok(myUserAccessTokens[testId]); + assert.ok(myUserAccessTokens[testId].is_active); + assert.ok(!myUserAccessTokens[testId].token); + }); + + it('clearUserAccessTokens', async () => { + await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState); + + const currentUserId = store.getState().entities.users.currentUserId; + + nock(Client4.getBaseRoute()). + post(`/users/${currentUserId}/tokens`). + reply(201, {id: 'someid', token: 'sometoken', description: 'test token', user_id: currentUserId}); + + await Actions.createUserAccessToken(currentUserId, 'test token')(store.dispatch, store.getState); + + await Actions.clearUserAccessTokens()(store.dispatch, store.getState); + + const {myUserAccessTokens} = store.getState().entities.users; + + assert.ok(Object.values(myUserAccessTokens).length === 0); + }); + + describe('checkForModifiedUsers', () => { + test('should request users by IDs that have changed since the last websocket disconnect', async () => { + const lastDisconnectAt = 1500; + + const user1 = {id: 'user1', update_at: 1000}; + const user2 = {id: 'user2', update_at: 1000}; + + nock(Client4.getBaseRoute()). + post('/users/ids'). + query({since: lastDisconnectAt}). + reply(200, [{...user2, update_at: 2000}]); + + store = await configureStore({ + entities: { + general: { + serverVersion: '5.14.0', + }, + users: { + profiles: { + user1, + user2, + }, + }, + }, + websocket: { + lastDisconnectAt, + }, + }); + + await store.dispatch(Actions.checkForModifiedUsers()); + + const profiles = store.getState().entities.users.profiles; + expect(profiles.user1).toBe(user1); + expect(profiles.user2).not.toBe(user2); + expect(profiles.user2).toEqual({id: 'user2', update_at: 2000}); + }); + + test('should do nothing on older servers', async () => { + const lastDisconnectAt = 1500; + const originalState = deepFreeze({ + entities: { + general: { + serverVersion: '5.13.0', + }, + users: { + profiles: {}, + }, + }, + websocket: { + lastDisconnectAt, + }, + }); + + store = await configureStore(originalState); + + await store.dispatch(Actions.checkForModifiedUsers()); + + const profiles = store.getState().entities.users.profiles; + expect(profiles).toBe(originalState.entities.users.profiles); + }); + }); +}); diff --git a/app/mm-redux/actions/users.ts b/app/mm-redux/actions/users.ts new file mode 100644 index 000000000..4c6d9a220 --- /dev/null +++ b/app/mm-redux/actions/users.ts @@ -0,0 +1,1469 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Action, ActionFunc, ActionResult, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; +import {UserProfile, UserStatus} from '@mm-redux/types/users'; +import {TeamMembership} from '@mm-redux/types/teams'; +import {Client4} from '@mm-redux/client'; +import {General} from '../constants'; +import {UserTypes, TeamTypes} from '@mm-redux/action_types'; +import {getAllCustomEmojis} from './emojis'; +import {getClientConfig, setServerVersion} from './general'; +import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from './teams'; +import {loadRolesIfNeeded} from './roles'; +import {getUserIdFromChannelName, isDirectChannel, isDirectChannelVisible, isGroupChannel, isGroupChannelVisible} from '@mm-redux/utils/channel_utils'; +import {removeUserFromList} from '@mm-redux/utils/user_utils'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; + +import {getConfig, getServerVersion} from '@mm-redux/selectors/entities/general'; + +import {getCurrentUserId, getUsers} from '@mm-redux/selectors/entities/users'; + +import {logError} from './errors'; +import {bindClientFunc, forceLogoutIfNecessary, debounce} from './helpers'; +import {getMyPreferences, makeDirectChannelVisibleIfNecessary, makeGroupMessageVisibleIfNecessary} from './preferences'; +import {Dictionary} from '@mm-redux/types/utilities'; +export function checkMfa(loginId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: UserTypes.CHECK_MFA_REQUEST, data: null}); + try { + const data = await Client4.checkUserMfa(loginId); + dispatch({type: UserTypes.CHECK_MFA_SUCCESS, data: null}); + return {data: data.mfa_required}; + } catch (error) { + dispatch(batchActions([ + {type: UserTypes.CHECK_MFA_FAILURE, error}, + logError(error), + ])); + return {error}; + } + }; +} + +export function generateMfaSecret(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.generateMfaSecret, + params: [ + userId, + ], + }); +} + +export function createUser(user: UserProfile, token: string, inviteId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let created; + + try { + created = await Client4.createUser(user, token, inviteId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const profiles: { + [userId: string]: UserProfile; + } = { + [created.id]: created, + }; + dispatch({type: UserTypes.RECEIVED_PROFILES, data: profiles}); + + return {data: created}; + }; +} + +export function login(loginId: string, password: string, mfaToken = '', ldapOnly = false): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: UserTypes.LOGIN_REQUEST, data: null}); + + const deviceId = getState().entities.general.deviceToken; + let data; + + try { + data = await Client4.login(loginId, password, mfaToken, deviceId, ldapOnly); + } catch (error) { + dispatch(batchActions([ + { + type: UserTypes.LOGIN_FAILURE, + error, + }, + logError(error), + ])); + return {error}; + } + + return completeLogin(data)(dispatch, getState); + }; +} + +export function loginById(id: string, password: string, mfaToken = ''): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: UserTypes.LOGIN_REQUEST, data: null}); + + const deviceId = getState().entities.general.deviceToken; + let data; + + try { + data = await Client4.loginById(id, password, mfaToken, deviceId); + } catch (error) { + dispatch(batchActions([ + { + type: UserTypes.LOGIN_FAILURE, + error, + }, + logError(error), + ])); + return {error}; + } + + return completeLogin(data)(dispatch, getState); + }; +} + +function completeLogin(data: UserProfile): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: UserTypes.RECEIVED_ME, + data, + }); + + Client4.setUserId(data.id); + Client4.setUserRoles(data.roles); + let teamMembers; + + try { + const membersRequest: Promise> = Client4.getMyTeamMembers(); + const unreadsRequest = Client4.getMyTeamUnreads(); + + teamMembers = await membersRequest; + const teamUnreads = await unreadsRequest; + + if (teamUnreads) { + for (const u of teamUnreads) { + const index = teamMembers.findIndex((m) => m.team_id === u.team_id); + const member = teamMembers[index]; + member.mention_count = u.mention_count; + member.msg_count = u.msg_count; + } + } + } catch (error) { + dispatch(batchActions([ + {type: UserTypes.LOGIN_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + const promises = [ + dispatch(getMyPreferences()), + dispatch(getMyTeams()), + dispatch(getClientConfig()), + ]; + + const serverVersion = Client4.getServerVersion(); + dispatch(setServerVersion(serverVersion)); + if (!isMinimumServerVersion(serverVersion, 4, 7) && getConfig(getState()).EnableCustomEmoji === 'true') { + dispatch(getAllCustomEmojis()); + } + + try { + await Promise.all(promises); + } catch (error) { + dispatch(batchActions([ + {type: UserTypes.LOGIN_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS, + data: teamMembers, + }, + { + type: UserTypes.LOGIN_SUCCESS, + }, + ])); + const roles = new Set(); + for (const role of data.roles.split(' ')) { + roles.add(role); + } + for (const teamMember of teamMembers) { + for (const role of teamMember.roles.split(' ')) { + roles.add(role); + } + } + if (roles.size > 0) { + dispatch(loadRolesIfNeeded(roles)); + } + + return {data: true}; + }; +} + +export function loadMe(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const config = getConfig(state); + + const deviceId = state.entities.general.deviceToken; + if (deviceId) { + Client4.attachDevice(deviceId); + } + + const promises = [ + dispatch(getMe()), + dispatch(getMyPreferences()), + dispatch(getMyTeams()), + dispatch(getMyTeamMembers()), + dispatch(getMyTeamUnreads()), + ]; + + // Sometimes the server version is set in one or the other + const serverVersion = Client4.getServerVersion() || getState().entities.general.serverVersion; + dispatch(setServerVersion(serverVersion)); + if (!isMinimumServerVersion(serverVersion, 4, 7) && config.EnableCustomEmoji === 'true') { + dispatch(getAllCustomEmojis()); + } + + await Promise.all(promises); + + const {currentUserId} = getState().entities.users; + const user = getState().entities.users.profiles[currentUserId]; + if (currentUserId) { + Client4.setUserId(currentUserId); + } + + if (user) { + Client4.setUserRoles(user.roles); + } + + return {data: true}; + }; +} + +export function logout(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: UserTypes.LOGOUT_REQUEST, data: null}); + + try { + await Client4.logout(); + } catch (error) { + // nothing to do here + } + + dispatch({type: UserTypes.LOGOUT_SUCCESS, data: null}); + + return {data: true}; + }; +} + +export function getTotalUsersStats(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTotalUsersStats, + onSuccess: UserTypes.RECEIVED_USER_STATS, + }); +} + +export function getProfiles(page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, options: any = {}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let profiles: UserProfile[]; + + try { + profiles = await Client4.getProfiles(page, perPage, options); + removeUserFromList(currentUserId, profiles); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.RECEIVED_PROFILES_LIST, + data: profiles, + }); + + return {data: profiles}; + }; +} + +export function getMissingProfilesByIds(userIds: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {profiles} = getState().entities.users; + const missingIds: string[] = []; + userIds.forEach((id) => { + if (!profiles[id]) { + missingIds.push(id); + } + }); + + if (missingIds.length > 0) { + getStatusesByIds(missingIds)(dispatch, getState); + return getProfilesByIds(missingIds)(dispatch, getState); + } + + return {data: []}; + }; +} + +export function getMissingProfilesByUsernames(usernames: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {profiles} = getState().entities.users; + + const usernameProfiles = Object.values(profiles).reduce((acc, profile: any) => { + acc[profile.username] = profile; + return acc; + }, {} as Dictionary); + const missingUsernames: string[] = []; + usernames.forEach((username) => { + if (!usernameProfiles[username]) { + missingUsernames.push(username); + } + }); + + if (missingUsernames.length > 0) { + return getProfilesByUsernames(missingUsernames)(dispatch, getState); + } + + return {data: []}; + }; +} + +export function getProfilesByIds(userIds: Array, options?: any): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let profiles: UserProfile[]; + + try { + profiles = await Client4.getProfilesByIds(userIds, options); + removeUserFromList(currentUserId, profiles); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.RECEIVED_PROFILES_LIST, + data: profiles, + }); + + return {data: profiles}; + }; +} + +export function getProfilesByUsernames(usernames: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let profiles; + + try { + profiles = await Client4.getProfilesByUsernames(usernames); + removeUserFromList(currentUserId, profiles); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.RECEIVED_PROFILES_LIST, + data: profiles, + }); + + return {data: profiles}; + }; +} + +export function getProfilesInTeam(teamId: string, page: number, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: any = {}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let profiles; + + try { + profiles = await Client4.getProfilesInTeam(teamId, page, perPage, sort, options); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM, + data: profiles, + id: teamId, + }, + { + type: UserTypes.RECEIVED_PROFILES_LIST, + data: removeUserFromList(currentUserId, [...profiles]), + }, + ])); + + return {data: profiles}; + }; +} + +export function getProfilesNotInTeam(teamId: string, groupConstrained: boolean, page: number, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let profiles; + try { + profiles = await Client4.getProfilesNotInTeam(teamId, groupConstrained, page, perPage); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const receivedProfilesListActionType = groupConstrained ? + UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM_AND_REPLACE : + UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM; + + dispatch(batchActions([ + { + type: receivedProfilesListActionType, + data: profiles, + id: teamId, + }, + { + type: UserTypes.RECEIVED_PROFILES_LIST, + data: profiles, + }, + ])); + + return {data: profiles}; + }; +} + +export function getProfilesWithoutTeam(page: number, perPage: number = General.PROFILE_CHUNK_SIZE, options: any = {}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let profiles = null; + try { + profiles = await Client4.getProfilesWithoutTeam(page, perPage, options); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: UserTypes.RECEIVED_PROFILES_LIST_WITHOUT_TEAM, + data: profiles, + }, + { + type: UserTypes.RECEIVED_PROFILES_LIST, + data: profiles, + }, + ])); + + return {data: profiles}; + }; +} + +export function getProfilesInChannel(channelId: string, page: number, perPage: number = General.PROFILE_CHUNK_SIZE, sort = ''): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let profiles; + + try { + profiles = await Client4.getProfilesInChannel(channelId, page, perPage, sort); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch(batchActions([ + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, + data: profiles, + id: channelId, + }, + { + type: UserTypes.RECEIVED_PROFILES_LIST, + data: removeUserFromList(currentUserId, [...profiles]), + }, + ])); + + return {data: profiles}; + }; +} + +export function getProfilesInGroupChannels(channelsIds: Array): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let channelProfiles; + + try { + channelProfiles = await Client4.getProfilesInGroupChannels(channelsIds.slice(0, General.MAX_GROUP_CHANNELS_FOR_PROFILES)); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const actions: Action[] = []; + for (const channelId in channelProfiles) { + if (channelProfiles.hasOwnProperty(channelId)) { + const profiles = channelProfiles[channelId]; + + actions.push( + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, + data: profiles, + id: channelId, + }, + { + type: UserTypes.RECEIVED_PROFILES_LIST, + data: removeUserFromList(currentUserId, [...profiles]), + }, + ); + } + } + + dispatch(batchActions(actions)); + + return {data: channelProfiles}; + }; +} + +export function getProfilesNotInChannel(teamId: string, channelId: string, groupConstrained: boolean, page: number, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + let profiles; + + try { + profiles = await Client4.getProfilesNotInChannel(teamId, channelId, groupConstrained, page, perPage); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const receivedProfilesListActionType = groupConstrained ? + UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL_AND_REPLACE : + UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL; + + dispatch(batchActions([ + { + type: receivedProfilesListActionType, + data: profiles, + id: channelId, + }, + { + type: UserTypes.RECEIVED_PROFILES_LIST, + data: removeUserFromList(currentUserId, [...profiles]), + }, + ])); + + return {data: profiles}; + }; +} + +export function getMe(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const getMeFunc = bindClientFunc({ + clientFunc: Client4.getMe, + onSuccess: UserTypes.RECEIVED_ME, + }); + const me = await getMeFunc(dispatch, getState); + + if ('error' in me) { + return me; + } + if ('data' in me) { + dispatch(loadRolesIfNeeded(me.data.roles.split(' '))); + } + return me; + }; +} + +export function updateMyTermsOfServiceStatus(termsOfServiceId: string, accepted: boolean): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const response: ActionResult = await dispatch(bindClientFunc({ + clientFunc: Client4.updateMyTermsOfServiceStatus, + params: [ + termsOfServiceId, + accepted, + ], + })); + + if ('data' in response) { + if (accepted) { + dispatch({ + type: UserTypes.RECEIVED_TERMS_OF_SERVICE_STATUS, + data: { + terms_of_service_create_at: new Date().getTime(), + terms_of_service_id: accepted ? termsOfServiceId : null, + user_id: getCurrentUserId(getState()), + }, + }); + } + + return { + data: response.data, + }; + } + + return { + error: response.error, + }; + }; +} + +export function getTermsOfService(): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getTermsOfService, + }); +} + +export function promoteGuestToUser(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.promoteGuestToUser, + params: [userId], + }); +} + +export function demoteUserToGuest(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.demoteUserToGuest, + params: [userId], + }); +} + +export function createTermsOfService(text: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.createTermsOfService, + params: [ + text, + ], + }); +} + +export function getUser(id: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getUser, + onSuccess: UserTypes.RECEIVED_PROFILE, + params: [ + id, + ], + }); +} + +export function getUserByUsername(username: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getUserByUsername, + onSuccess: UserTypes.RECEIVED_PROFILE, + params: [ + username, + ], + }); +} + +export function getUserByEmail(email: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getUserByEmail, + onSuccess: UserTypes.RECEIVED_PROFILE, + params: [ + email, + ], + }); +} + +// We create an array to hold the id's that we want to get a status for. We build our +// debounced function that will get called after a set period of idle time in which +// the array of id's will be passed to the getStatusesByIds with a cb that clears out +// the array. Helps with performance because instead of making 75 different calls for +// statuses, we are only making one call for 75 ids. +// We could maybe clean it up somewhat by storing the array of ids in redux state possbily? +let ids: Array = []; +const debouncedGetStatusesByIds = debounce(async (dispatch: DispatchFunc, getState: GetStateFunc) => { + getStatusesByIds([...new Set(ids)])(dispatch, getState); +}, 20, false, () => { + ids = []; +}); +export function getStatusesByIdsBatchedDebounced(id: string) { + ids = [...ids, id]; + return debouncedGetStatusesByIds; +} + +export function getStatusesByIds(userIds: Array): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getStatusesByIds, + onSuccess: UserTypes.RECEIVED_STATUSES, + params: [ + userIds, + ], + }); +} + +export function getStatus(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getStatus, + onSuccess: UserTypes.RECEIVED_STATUS, + params: [ + userId, + ], + }); +} + +export function setStatus(status: UserStatus): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateStatus(status); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.RECEIVED_STATUS, + data: status, + }); + + return {data: status}; + }; +} + +export function getSessions(userId: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getSessions, + onSuccess: UserTypes.RECEIVED_SESSIONS, + params: [ + userId, + ], + }); +} + +export function revokeSession(userId: string, sessionId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.revokeSession(userId, sessionId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.RECEIVED_REVOKED_SESSION, + sessionId, + data: null, + }); + + return {data: true}; + }; +} + +export function revokeAllSessionsForUser(userId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.revokeAllSessionsForUser(userId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + const data = {isCurrentUser: userId === getCurrentUserId(getState())}; + dispatch(batchActions([ + { + type: UserTypes.REVOKE_ALL_USER_SESSIONS_SUCCESS, + data, + }, + ])); + + return {data: true}; + }; +} + +export function revokeSessionsForAllUsers(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.revokeSessionsForAllUsers(); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + dispatch({ + type: UserTypes.REVOKE_SESSIONS_FOR_ALL_USERS_SUCCESS, + data: null, + }); + return {data: true}; + }; +} + +export function loadProfilesForDirect(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const config = state.entities.general.config; + const {channels, myMembers} = state.entities.channels; + const {myPreferences} = state.entities.preferences; + const {currentUserId, profiles} = state.entities.users; + + const values = Object.values(channels); + for (let i = 0; i < values.length; i++) { + const channel: any = values[i]; + const member = myMembers[channel.id]; + if (!isDirectChannel(channel) && !isGroupChannel(channel)) { + continue; + } + + if (member) { + if (member.mention_count > 0 && isDirectChannel(channel)) { + const otherUserId = getUserIdFromChannelName(currentUserId, channel.name); + if (!isDirectChannelVisible(profiles[otherUserId] || otherUserId, config, myPreferences, channel)) { + makeDirectChannelVisibleIfNecessary(otherUserId)(dispatch, getState); + } + } else if ((member.mention_count > 0 || member.msg_count < channel.total_msg_count) && + isGroupChannel(channel) && !isGroupChannelVisible(config, myPreferences, channel)) { + makeGroupMessageVisibleIfNecessary(channel.id)(dispatch, getState); + } + } + } + + return {data: true}; + }; +} + +export function getUserAudits(userId: string, page = 0, perPage: number = General.AUDITS_CHUNK_SIZE): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.getUserAudits, + onSuccess: UserTypes.RECEIVED_AUDITS, + params: [ + userId, + page, + perPage, + ], + }); +} + +export function autocompleteUsers(term: string, teamId = '', channelId = '', options: { + limit: number; +} = { + limit: General.AUTOCOMPLETE_LIMIT_DEFAULT, +}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: UserTypes.AUTOCOMPLETE_USERS_REQUEST, data: null}); + + const {currentUserId} = getState().entities.users; + + let data; + try { + data = await Client4.autocompleteUsers(term, teamId, channelId, options); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(batchActions([ + {type: UserTypes.AUTOCOMPLETE_USERS_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + let users = [...data.users]; + if (data.out_of_channel) { + users = [...users, ...data.out_of_channel]; + } + removeUserFromList(currentUserId, users); + const actions: Action[] = [{ + type: UserTypes.RECEIVED_PROFILES_LIST, + data: users, + }, { + type: UserTypes.AUTOCOMPLETE_USERS_SUCCESS, + }]; + + if (channelId) { + actions.push( + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, + data: data.users, + id: channelId, + } + ); + actions.push( + { + type: UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL, + data: data.out_of_channel, + id: channelId, + } + ); + } + + if (teamId) { + actions.push( + { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM, + data: users, + id: teamId, + } + ); + } + + dispatch(batchActions(actions)); + + return {data}; + }; +} + +export function searchProfiles(term: string, options: any = {}): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const {currentUserId} = getState().entities.users; + + let profiles; + try { + profiles = await Client4.searchUsers(term, options); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const actions: Action[] = [{type: UserTypes.RECEIVED_PROFILES_LIST, data: removeUserFromList(currentUserId, [...profiles])}]; + + if (options.in_channel_id) { + actions.push({ + type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, + data: profiles, + id: options.in_channel_id, + }); + } + + if (options.not_in_channel_id) { + actions.push({ + type: UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL, + data: profiles, + id: options.not_in_channel_id, + }); + } + + if (options.team_id) { + actions.push({ + type: UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM, + data: profiles, + id: options.team_id, + }); + } + + if (options.not_in_team_id) { + actions.push({ + type: UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM, + data: profiles, + id: options.not_in_team_id, + }); + } + + dispatch(batchActions(actions)); + + return {data: profiles}; + }; +} + +let statusIntervalId: NodeJS.Timeout|null; +export function startPeriodicStatusUpdates(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + if (statusIntervalId) { + clearInterval(statusIntervalId); + } + + statusIntervalId = setInterval( + () => { + const {statuses} = getState().entities.users; + + if (!statuses) { + return; + } + + const userIds = Object.keys(statuses); + if (!userIds.length) { + return; + } + + getStatusesByIds(userIds)(dispatch, getState); + }, + General.STATUS_INTERVAL + ); + + return {data: true}; + }; +} + +export function stopPeriodicStatusUpdates(): ActionFunc { + return async () => { + if (statusIntervalId) { + clearInterval(statusIntervalId); + } + + return {data: true}; + }; +} + +export function updateMe(user: UserProfile): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({type: UserTypes.UPDATE_ME_REQUEST, data: null}); + + let data; + try { + data = await Client4.patchMe(user); + } catch (error) { + dispatch(batchActions([ + {type: UserTypes.UPDATE_ME_FAILURE, error}, + logError(error), + ])); + return {error}; + } + + dispatch(batchActions([ + {type: UserTypes.RECEIVED_ME, data}, + {type: UserTypes.UPDATE_ME_SUCCESS}, + ])); + dispatch(loadRolesIfNeeded(data.roles.split(' '))); + + return {data}; + }; +} + +export function patchUser(user: UserProfile): ActionFunc { + return async (dispatch: DispatchFunc) => { + let data: UserProfile; + try { + data = await Client4.patchUser(user); + } catch (error) { + dispatch(logError(error)); + return {error}; + } + + dispatch({type: UserTypes.RECEIVED_PROFILE, data}); + + return {data}; + }; +} + +export function updateUserRoles(userId: string, roles: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateUserRoles(userId, roles); + } catch (error) { + return {error}; + } + + const profile = getState().entities.users.profiles[userId]; + if (profile) { + dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, roles}}); + } + + return {data: true}; + }; +} + +export function updateUserMfa(userId: string, activate: boolean, code = ''): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateUserMfa(userId, activate, code); + } catch (error) { + dispatch(logError(error)); + return {error}; + } + + const profile = getState().entities.users.profiles[userId]; + if (profile) { + dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, mfa_active: activate}}); + } + + return {data: true}; + }; +} + +export function updateUserPassword(userId: string, currentPassword: string, newPassword: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateUserPassword(userId, currentPassword, newPassword); + } catch (error) { + dispatch(logError(error)); + return {error}; + } + + const profile = getState().entities.users.profiles[userId]; + if (profile) { + dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, last_password_update_at: new Date().getTime()}}); + } + + return {data: true}; + }; +} + +export function updateUserActive(userId: string, active: boolean): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.updateUserActive(userId, active); + } catch (error) { + dispatch(logError(error)); + return {error}; + } + + const profile = getState().entities.users.profiles[userId]; + if (profile) { + const deleteAt = active ? 0 : new Date().getTime(); + dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, delete_at: deleteAt}}); + } + + return {data: true}; + }; +} + +export function verifyUserEmail(token: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.verifyUserEmail, + params: [ + token, + ], + }); +} + +export function sendVerificationEmail(email: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.sendVerificationEmail, + params: [ + email, + ], + }); +} + +export function resetUserPassword(token: string, newPassword: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.resetUserPassword, + params: [ + token, + newPassword, + ], + }); +} + +export function sendPasswordResetEmail(email: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.sendPasswordResetEmail, + params: [ + email, + ], + }); +} + +export function setDefaultProfileImage(userId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.setDefaultProfileImage(userId); + } catch (error) { + dispatch(logError(error)); + return {error}; + } + + const profile = getState().entities.users.profiles[userId]; + if (profile) { + dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, last_picture_update: 0}}); + } + + return {data: true}; + }; +} + +export function uploadProfileImage(userId: string, imageData: any): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.uploadProfileImage(userId, imageData); + } catch (error) { + return {error}; + } + + const profile = getState().entities.users.profiles[userId]; + if (profile) { + dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, last_picture_update: new Date().getTime()}}); + } + + return {data: true}; + }; +} + +export function switchEmailToOAuth(service: string, email: string, password: string, mfaCode = ''): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.switchEmailToOAuth, + params: [ + service, + email, + password, + mfaCode, + ], + }); +} + +export function switchOAuthToEmail(currentService: string, email: string, password: string): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.switchOAuthToEmail, + params: [ + currentService, + email, + password, + ], + }); +} + +export function switchEmailToLdap(email: string, emailPassword: string, ldapId: string, ldapPassword: string, mfaCode = ''): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.switchEmailToLdap, + params: [ + email, + emailPassword, + ldapId, + ldapPassword, + mfaCode, + ], + }); +} + +export function switchLdapToEmail(ldapPassword: string, email: string, emailPassword: string, mfaCode = ''): ActionFunc { + return bindClientFunc({ + clientFunc: Client4.switchLdapToEmail, + params: [ + ldapPassword, + email, + emailPassword, + mfaCode, + ], + }); +} + +export function createUserAccessToken(userId: string, description: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + + try { + data = await Client4.createUserAccessToken(userId, description); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const actions: Action[] = []; + + const {currentUserId} = getState().entities.users; + if (userId === currentUserId) { + actions.push( + { + type: UserTypes.RECEIVED_MY_USER_ACCESS_TOKEN, + data: {...data, token: ''}, + } + ); + } + + dispatch(batchActions(actions)); + + return {data}; + }; +} + +export function getUserAccessToken(tokenId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getUserAccessToken(tokenId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const actions: Action[] = []; + + const {currentUserId} = getState().entities.users; + if (data.user_id === currentUserId) { + actions.push( + { + type: UserTypes.RECEIVED_MY_USER_ACCESS_TOKEN, + data, + } + ); + } + + dispatch(batchActions(actions)); + + return {data}; + }; +} + +export function getUserAccessTokens(page = 0, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + + try { + data = await Client4.getUserAccessTokens(page, perPage); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + return {data}; + }; +} + +export function getUserAccessTokensForUser(userId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getUserAccessTokensForUser(userId, page, perPage); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + const actions: Action[] = []; + + const {currentUserId} = getState().entities.users; + if (userId === currentUserId) { + actions.push( + { + type: UserTypes.RECEIVED_MY_USER_ACCESS_TOKENS, + data, + } + ); + } + + dispatch(batchActions(actions)); + + return {data}; + }; +} + +export function revokeUserAccessToken(tokenId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.revokeUserAccessToken(tokenId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.REVOKED_USER_ACCESS_TOKEN, + data: tokenId, + }); + + return {data: true}; + }; +} + +export function disableUserAccessToken(tokenId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.disableUserAccessToken(tokenId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.DISABLED_USER_ACCESS_TOKEN, + data: tokenId, + }); + + return {data: true}; + }; +} + +export function enableUserAccessToken(tokenId: string): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + try { + await Client4.enableUserAccessToken(tokenId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {error}; + } + + dispatch({ + type: UserTypes.ENABLED_USER_ACCESS_TOKEN, + data: tokenId, + }); + + return {data: true}; + }; +} + +export function clearUserAccessTokens(): ActionFunc { + return async (dispatch) => { + dispatch({type: UserTypes.CLEAR_MY_USER_ACCESS_TOKENS, data: null}); + return {data: true}; + }; +} + +export function checkForModifiedUsers() { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + const users = getUsers(state); + const lastDisconnectAt = state.websocket.lastDisconnectAt; + const serverVersion = getServerVersion(state); + + if (!isMinimumServerVersion(serverVersion, 5, 14)) { + return {data: true}; + } + + await dispatch(getProfilesByIds(Object.keys(users), {since: lastDisconnectAt})); + return {data: true}; + }; +} + +export default { + checkMfa, + generateMfaSecret, + login, + logout, + getProfiles, + getProfilesByIds, + getProfilesInTeam, + getProfilesInChannel, + getProfilesNotInChannel, + getUser, + getMe, + getUserByUsername, + getStatus, + getStatusesByIds, + getSessions, + getTotalUsersStats, + loadProfilesForDirect, + revokeSession, + revokeAllSessionsForUser, + revokeSessionsForAllUsers, + getUserAudits, + searchProfiles, + startPeriodicStatusUpdates, + stopPeriodicStatusUpdates, + updateMe, + updateUserRoles, + updateUserMfa, + updateUserPassword, + updateUserActive, + verifyUserEmail, + sendVerificationEmail, + resetUserPassword, + sendPasswordResetEmail, + uploadProfileImage, + switchEmailToOAuth, + switchOAuthToEmail, + switchEmailToLdap, + switchLdapToEmail, + getTermsOfService, + createTermsOfService, + updateMyTermsOfServiceStatus, + createUserAccessToken, + getUserAccessToken, + getUserAccessTokensForUser, + revokeUserAccessToken, + disableUserAccessToken, + enableUserAccessToken, + checkForModifiedUsers, +}; diff --git a/app/mm-redux/client/client4.test.js b/app/mm-redux/client/client4.test.js new file mode 100644 index 000000000..b11671338 --- /dev/null +++ b/app/mm-redux/client/client4.test.js @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import nock from 'nock'; + +import {ClientError, HEADER_X_VERSION_ID} from '@mm-redux/client/client4'; +import TestHelper from 'test/test_helper'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; + +describe('Client4', () => { + beforeAll(() => { + if (!nock.isActive()) { + nock.activate(); + } + }); + + afterAll(() => { + nock.restore(); + }); + + describe('doFetchWithResponse', () => { + it('serverVersion should be set from response header', async () => { + const client = TestHelper.createClient(); + + assert.equal(client.serverVersion, ''); + + nock(client.getBaseRoute()). + get('/users/me'). + reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.0.0.5.0.0.abc123'}); + + await client.getMe(); + + assert.equal(client.serverVersion, '5.0.0.5.0.0.abc123'); + assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true); + assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), false); + + nock(client.getBaseRoute()). + get('/users/me'). + reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.3.0.5.3.0.abc123'}); + + await client.getMe(); + + assert.equal(client.serverVersion, '5.3.0.5.3.0.abc123'); + assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true); + assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), true); + }); + }); +}); + +describe('ClientError', () => { + it('standard fields should be enumerable', () => { + const error = new ClientError('https://example.com', { + message: 'This is a message', + intl: { + id: 'test.error', + defaultMessage: 'This is a message with a translation', + }, + server_error_id: 'test.app_error', + status_code: 418, + url: 'https://example.com/api/v4/error', + }); + + const copy = {...error}; + + assert.strictEqual(copy.message, error.message); + assert.strictEqual(copy.intl, error.intl); + assert.strictEqual(copy.server_error_id, error.server_error_id); + assert.strictEqual(copy.status_code, error.status_code); + assert.strictEqual(copy.url, error.url); + }); +}); diff --git a/app/mm-redux/client/client4.ts b/app/mm-redux/client/client4.ts new file mode 100644 index 000000000..e429bfe23 --- /dev/null +++ b/app/mm-redux/client/client4.ts @@ -0,0 +1,3177 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {buildQueryString, isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {cleanUrlForLogging} from '@mm-redux/utils/sentry'; +import {General} from '../constants'; + +import {isSystemAdmin} from '@mm-redux/utils/user_utils'; + +import fetch from './fetch_etag'; +import {UserProfile, UserStatus} from '@mm-redux/types/users'; +import {Team} from '@mm-redux/types/teams'; +import {Channel, ChannelModerationPatch} from '@mm-redux/types/channels'; +import {Post} from '@mm-redux/types/posts'; +import {Job} from '@mm-redux/types/jobs'; +import {Role} from '@mm-redux/types/roles'; +import {Scheme} from '@mm-redux/types/schemes'; +import {Options} from '@mm-redux/types/client4'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {IncomingWebhook, OutgoingWebhook, Command, OAuthApp, DialogSubmission} from '@mm-redux/types/integrations'; +import {CustomEmoji} from '@mm-redux/types/emojis'; +import {Config} from '@mm-redux/types/config'; +import {Bot, BotPatch} from '@mm-redux/types/bots'; +import {Dictionary} from '@mm-redux/types/utilities'; +import {SyncablePatch} from '@mm-redux/types/groups'; + +const FormData = require('form-data'); +const HEADER_AUTH = 'Authorization'; +const HEADER_BEARER = 'BEARER'; +const HEADER_REQUESTED_WITH = 'X-Requested-With'; +const HEADER_USER_AGENT = 'User-Agent'; +const HEADER_X_CLUSTER_ID = 'X-Cluster-Id'; +const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token'; +export const HEADER_X_VERSION_ID = 'X-Version-Id'; +const PER_PAGE_DEFAULT = 60; +const LOGS_PER_PAGE_DEFAULT = 10000; +export const DEFAULT_LIMIT_BEFORE = 30; +export const DEFAULT_LIMIT_AFTER = 30; + +/* eslint-disable no-throw-literal */ + +export default class Client4 { + logToConsole = false; + serverVersion = ''; + clusterId = ''; + token = ''; + csrf = ''; + url = ''; + urlVersion = '/api/v4'; + userAgent: string|null = null; + enableLogging = false; + defaultHeaders: {[x: string]: string} = {}; + userId = ''; + diagnosticId = ''; + includeCookies = true; + translations = { + connectionError: 'There appears to be a problem with your internet connection.', + unknownError: 'We received an unexpected status code from the server.', + }; + userRoles?: string; + + getUrl() { + return this.url; + } + + getAbsoluteUrl(baseUrl: string) { + if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) { + return baseUrl; + } + return this.getUrl() + baseUrl; + } + + setUrl(url: string) { + this.url = url; + } + + setUserAgent(userAgent: string) { + this.userAgent = userAgent; + } + + getToken() { + return this.token; + } + + setToken(token: string) { + this.token = token; + } + + setCSRF(csrfToken: string) { + this.csrf = csrfToken; + } + + setAcceptLanguage(locale: string) { + this.defaultHeaders['Accept-Language'] = locale; + } + + setEnableLogging(enable: boolean) { + this.enableLogging = enable; + } + + setIncludeCookies(include: boolean) { + this.includeCookies = include; + } + + setUserId(userId: string) { + this.userId = userId; + } + + setUserRoles(roles: string) { + this.userRoles = roles; + } + + setDiagnosticId(diagnosticId: string) { + this.diagnosticId = diagnosticId; + } + + getServerVersion() { + return this.serverVersion; + } + + getUrlVersion() { + return this.urlVersion; + } + + getBaseRoute() { + return `${this.url}${this.urlVersion}`; + } + + getUsersRoute() { + return `${this.getBaseRoute()}/users`; + } + + getUserRoute(userId: string) { + return `${this.getUsersRoute()}/${userId}`; + } + + getTeamsRoute() { + return `${this.getBaseRoute()}/teams`; + } + + getTeamRoute(teamId: string) { + return `${this.getTeamsRoute()}/${teamId}`; + } + + getTeamSchemeRoute(teamId: string) { + return `${this.getTeamRoute(teamId)}/scheme`; + } + + getTeamNameRoute(teamName: string) { + return `${this.getTeamsRoute()}/name/${teamName}`; + } + + getTeamMembersRoute(teamId: string) { + return `${this.getTeamRoute(teamId)}/members`; + } + + getTeamMemberRoute(teamId: string, userId: string) { + return `${this.getTeamMembersRoute(teamId)}/${userId}`; + } + + getChannelsRoute() { + return `${this.getBaseRoute()}/channels`; + } + + getChannelRoute(channelId: string) { + return `${this.getChannelsRoute()}/${channelId}`; + } + + getChannelMembersRoute(channelId: string) { + return `${this.getChannelRoute(channelId)}/members`; + } + + getChannelMemberRoute(channelId: string, userId: string) { + return `${this.getChannelMembersRoute(channelId)}/${userId}`; + } + + getChannelSchemeRoute(channelId: string) { + return `${this.getChannelRoute(channelId)}/scheme`; + } + + getPostsRoute() { + return `${this.getBaseRoute()}/posts`; + } + + getPostRoute(postId: string) { + return `${this.getPostsRoute()}/${postId}`; + } + + getReactionsRoute() { + return `${this.getBaseRoute()}/reactions`; + } + + getCommandsRoute() { + return `${this.getBaseRoute()}/commands`; + } + + getFilesRoute() { + return `${this.getBaseRoute()}/files`; + } + + getFileRoute(fileId: string) { + return `${this.getFilesRoute()}/${fileId}`; + } + + getPreferencesRoute(userId: string) { + return `${this.getUserRoute(userId)}/preferences`; + } + + getIncomingHooksRoute() { + return `${this.getBaseRoute()}/hooks/incoming`; + } + + getIncomingHookRoute(hookId: string) { + return `${this.getBaseRoute()}/hooks/incoming/${hookId}`; + } + + getOutgoingHooksRoute() { + return `${this.getBaseRoute()}/hooks/outgoing`; + } + + getOutgoingHookRoute(hookId: string) { + return `${this.getBaseRoute()}/hooks/outgoing/${hookId}`; + } + + getOAuthRoute() { + return `${this.url}/oauth`; + } + + getOAuthAppsRoute() { + return `${this.getBaseRoute()}/oauth/apps`; + } + + getOAuthAppRoute(appId: string) { + return `${this.getOAuthAppsRoute()}/${appId}`; + } + + getEmojisRoute() { + return `${this.getBaseRoute()}/emoji`; + } + + getEmojiRoute(emojiId: string) { + return `${this.getEmojisRoute()}/${emojiId}`; + } + + getBrandRoute() { + return `${this.getBaseRoute()}/brand`; + } + + getBrandImageUrl(timestamp: string) { + return `${this.getBrandRoute()}/image?t=${timestamp}`; + } + + getDataRetentionRoute() { + return `${this.getBaseRoute()}/data_retention`; + } + + getJobsRoute() { + return `${this.getBaseRoute()}/jobs`; + } + + getPluginsRoute() { + return `${this.getBaseRoute()}/plugins`; + } + + getPluginRoute(pluginId: string) { + return `${this.getPluginsRoute()}/${pluginId}`; + } + + getPluginsMarketplaceRoute() { + return `${this.getPluginsRoute()}/marketplace`; + } + + getRolesRoute() { + return `${this.getBaseRoute()}/roles`; + } + + getTimezonesRoute() { + return `${this.getBaseRoute()}/system/timezones`; + } + + getSchemesRoute() { + return `${this.getBaseRoute()}/schemes`; + } + + getRedirectLocationRoute() { + return `${this.getBaseRoute()}/redirect_location`; + } + + getBotsRoute() { + return `${this.getBaseRoute()}/bots`; + } + + getBotRoute(botUserId: string) { + return `${this.getBotsRoute()}/${botUserId}`; + } + + getCSRFFromCookie() { + + // NOT NEEDED IN THE MOBILE APP + // if (typeof document !== 'undefined' && typeof document.cookie !== 'undefined') { + // const cookies = document.cookie.split(';'); + // for (let i = 0; i < cookies.length; i++) { + // const cookie = cookies[i].trim(); + // if (cookie.startsWith('MMCSRF=')) { + // return cookie.replace('MMCSRF=', ''); + // } + // } + // } + return ''; + } + + getOptions(options: Options) { + const newOptions: Options = {...options}; + + const headers: {[x: string]: string} = { + [HEADER_REQUESTED_WITH]: 'XMLHttpRequest', + ...this.defaultHeaders, + }; + + if (this.token) { + headers[HEADER_AUTH] = `${HEADER_BEARER} ${this.token}`; + } + + const csrfToken = this.csrf || this.getCSRFFromCookie(); + if (options.method && options.method.toLowerCase() !== 'get' && csrfToken) { + headers[HEADER_X_CSRF_TOKEN] = csrfToken; + } + + if (this.includeCookies) { + newOptions.credentials = 'include'; + } + + if (this.userAgent) { + headers[HEADER_USER_AGENT] = this.userAgent; + } + + if (newOptions.headers) { + Object.assign(headers, newOptions.headers); + } + + return { + ...newOptions, + headers, + }; + } + + // User Routes + + createUser = async (user: UserProfile, token: string, inviteId: string) => { + this.trackEvent('api', 'api_users_create'); + + const queryParams: any = {}; + + if (token) { + queryParams.t = token; + } + + if (inviteId) { + queryParams.iid = inviteId; + } + + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString(queryParams)}`, + {method: 'post', body: JSON.stringify(user)} + ); + } + + patchMe = async (userPatch: Partial) => { + return this.doFetch( + `${this.getUserRoute('me')}/patch`, + {method: 'put', body: JSON.stringify(userPatch)} + ); + } + + patchUser = async (userPatch: Partial & {id: string}) => { + this.trackEvent('api', 'api_users_patch'); + + return this.doFetch( + `${this.getUserRoute(userPatch.id)}/patch`, + {method: 'put', body: JSON.stringify(userPatch)} + ); + } + + updateUser = async (user: UserProfile) => { + this.trackEvent('api', 'api_users_update'); + + return this.doFetch( + `${this.getUserRoute(user.id)}`, + {method: 'put', body: JSON.stringify(user)} + ); + } + + promoteGuestToUser = async (userId: string) => { + this.trackEvent('api', 'api_users_promote_guest_to_user'); + + return this.doFetch( + `${this.getUserRoute(userId)}/promote`, + {method: 'post'} + ); + } + + demoteUserToGuest = async (userId: string) => { + this.trackEvent('api', 'api_users_demote_user_to_guest'); + + return this.doFetch( + `${this.getUserRoute(userId)}/demote`, + {method: 'post'} + ); + } + + updateUserRoles = async (userId: string, roles: string) => { + this.trackEvent('api', 'api_users_update_roles'); + + return this.doFetch( + `${this.getUserRoute(userId)}/roles`, + {method: 'put', body: JSON.stringify({roles})} + ); + }; + updateUserMfa = async (userId: string, activate: boolean, code: string) => { + const body: any = { + activate, + }; + + if (activate) { + body.code = code; + } + + return this.doFetch( + `${this.getUserRoute(userId)}/mfa`, + {method: 'put', body: JSON.stringify(body)} + ); + } + + updateUserPassword = async (userId: string, currentPassword: string, newPassword: string) => { + this.trackEvent('api', 'api_users_newpassword'); + + return this.doFetch( + `${this.getUserRoute(userId)}/password`, + {method: 'put', body: JSON.stringify({current_password: currentPassword, new_password: newPassword})} + ); + } + + resetUserPassword = async (token: string, newPassword: string) => { + this.trackEvent('api', 'api_users_reset_password'); + + return this.doFetch( + `${this.getUsersRoute()}/password/reset`, + {method: 'post', body: JSON.stringify({token, new_password: newPassword})} + ); + } + + sendPasswordResetEmail = async (email: string) => { + this.trackEvent('api', 'api_users_send_password_reset'); + + return this.doFetch( + `${this.getUsersRoute()}/password/reset/send`, + {method: 'post', body: JSON.stringify({email})} + ); + } + + updateUserActive = async (userId: string, active: boolean) => { + this.trackEvent('api', 'api_users_update_active'); + + return this.doFetch( + `${this.getUserRoute(userId)}/active`, + {method: 'put', body: JSON.stringify({active})} + ); + } + + uploadProfileImage = async (userId: string, imageData: any) => { + this.trackEvent('api', 'api_users_update_profile_picture'); + + const formData = new FormData(); + formData.append('image', imageData); + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + `${this.getUserRoute(userId)}/image`, + request + ); + }; + + setDefaultProfileImage = async (userId: string) => { + this.trackEvent('api', 'api_users_set_default_profile_picture'); + + return this.doFetch( + `${this.getUserRoute(userId)}/image`, + {method: 'delete'} + ); + }; + + verifyUserEmail = async (token: string) => { + return this.doFetch( + `${this.getUsersRoute()}/email/verify`, + {method: 'post', body: JSON.stringify({token})} + ); + } + + updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => { + return this.doFetch( + `${this.getUserRoute('me')}/terms_of_service`, + {method: 'post', body: JSON.stringify({termsOfServiceId, accepted})} + ); + } + + getTermsOfService = async () => { + return this.doFetch( + `${this.getBaseRoute()}/terms_of_service`, + {method: 'get'} + ); + } + + createTermsOfService = async (text: string) => { + return this.doFetch( + `${this.getBaseRoute()}/terms_of_service`, + {method: 'post', body: JSON.stringify({text})} + ); + } + + sendVerificationEmail = async (email: string) => { + return this.doFetch( + `${this.getUsersRoute()}/email/verify/send`, + {method: 'post', body: JSON.stringify({email})} + ); + } + + login = async (loginId: string, password: string, token = '', deviceId = '', ldapOnly = false) => { + this.trackEvent('api', 'api_users_login'); + + if (ldapOnly) { + this.trackEvent('api', 'api_users_login_ldap'); + } + + const body: any = { + device_id: deviceId, + login_id: loginId, + password, + token, + }; + + if (ldapOnly) { + body.ldap_only = 'true'; + } + + const {data} = await this.doFetchWithResponse( + `${this.getUsersRoute()}/login`, + {method: 'post', body: JSON.stringify(body)} + ); + + return data; + }; + + loginById = async (id: string, password: string, token = '', deviceId = '') => { + this.trackEvent('api', 'api_users_login'); + const body: any = { + device_id: deviceId, + id, + password, + token, + }; + + const {data} = await this.doFetchWithResponse( + `${this.getUsersRoute()}/login`, + {method: 'post', body: JSON.stringify(body)} + ); + + return data; + }; + + logout = async () => { + this.trackEvent('api', 'api_users_logout'); + + const {response} = await this.doFetchWithResponse( + `${this.getUsersRoute()}/logout`, + {method: 'post'} + ); + + if (response.ok) { + this.token = ''; + } + + this.serverVersion = ''; + + return response; + }; + + getProfiles = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => { + this.trackEvent('api', 'api_profiles_get'); + + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`, + {method: 'get'} + ); + }; + + getProfilesByIds = async (userIds: string[], options = {}) => { + this.trackEvent('api', 'api_profiles_get_by_ids'); + + return this.doFetch( + `${this.getUsersRoute()}/ids${buildQueryString(options)}`, + {method: 'post', body: JSON.stringify(userIds)} + ); + }; + + getProfilesByUsernames = async (usernames: string[]) => { + this.trackEvent('api', 'api_profiles_get_by_usernames'); + + return this.doFetch( + `${this.getUsersRoute()}/usernames`, + {method: 'post', body: JSON.stringify(usernames)} + ); + }; + + getProfilesInTeam = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '', options = {}) => { + this.trackEvent('api', 'api_profiles_get_in_team', {team_id: teamId, sort}); + + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString({...options, in_team: teamId, page, per_page: perPage, sort})}`, + {method: 'get'} + ); + }; + + getProfilesNotInTeam = async (teamId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained}); + + const queryStringObj: any = {not_in_team: teamId, page, per_page: perPage}; + if (groupConstrained) { + queryStringObj.group_constrained = true; + } + + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString(queryStringObj)}`, + {method: 'get'} + ); + }; + + getProfilesWithoutTeam = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => { + this.trackEvent('api', 'api_profiles_get_without_team'); + + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString({...options, without_team: 1, page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => { + this.trackEvent('api', 'api_profiles_get_in_channel', {channel_id: channelId}); + + const serverVersion = this.getServerVersion(); + let queryStringObj; + if (isMinimumServerVersion(serverVersion, 4, 7)) { + queryStringObj = {in_channel: channelId, page, per_page: perPage, sort}; + } else { + queryStringObj = {in_channel: channelId, page, per_page: perPage}; + } + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString(queryStringObj)}`, + {method: 'get'} + ); + }; + + getProfilesInGroupChannels = async (channelsIds: string[]) => { + this.trackEvent('api', 'api_profiles_get_in_group_channels', {channelsIds}); + + return this.doFetch( + `${this.getUsersRoute()}/group_channels`, + {method: 'post', body: JSON.stringify(channelsIds)} + ); + }; + + getProfilesNotInChannel = async (teamId: string, channelId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained}); + + const queryStringObj: any = {in_team: teamId, not_in_channel: channelId, page, per_page: perPage}; + if (groupConstrained) { + queryStringObj.group_constrained = true; + } + + return this.doFetch( + `${this.getUsersRoute()}${buildQueryString(queryStringObj)}`, + {method: 'get'} + ); + }; + + getMe = async () => { + return this.doFetch( + `${this.getUserRoute('me')}`, + {method: 'get'} + ); + }; + + getUser = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}`, + {method: 'get'} + ); + }; + + getUserByUsername = async (username: string) => { + return this.doFetch( + `${this.getUsersRoute()}/username/${username}`, + {method: 'get'} + ); + }; + + getUserByEmail = async (email: string) => { + return this.doFetch( + `${this.getUsersRoute()}/email/${email}`, + {method: 'get'} + ); + }; + + getProfilePictureUrl = (userId: string, lastPictureUpdate: number) => { + const params: any = {}; + + if (lastPictureUpdate) { + params._ = lastPictureUpdate; + } + + return `${this.getUserRoute(userId)}/image${buildQueryString(params)}`; + }; + getDefaultProfilePictureUrl = (userId: string) => { + return `${this.getUserRoute(userId)}/image/default`; + }; + autocompleteUsers = async (name: string, teamId: string, channelId: string, options = { + limit: General.AUTOCOMPLETE_LIMIT_DEFAULT, + }) => { + return this.doFetch(`${this.getUsersRoute()}/autocomplete${buildQueryString({ + in_team: teamId, + in_channel: channelId, + name, + limit: options.limit, + })}`, { + method: 'get', + }); + }; + getSessions = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/sessions`, + {method: 'get'} + ); + }; + + revokeSession = async (userId: string, sessionId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/sessions/revoke`, + {method: 'post', body: JSON.stringify({session_id: sessionId})} + ); + }; + + revokeAllSessionsForUser = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/sessions/revoke/all`, + {method: 'post'} + ); + }; + + revokeSessionsForAllUsers = async () => { + return this.doFetch( + `${this.getUsersRoute()}/sessions/revoke/all`, + {method: 'post'} + ); + }; + + getUserAudits = async (userId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getUserRoute(userId)}/audits${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + checkUserMfa = async (loginId: string) => { + return this.doFetch( + `${this.getUsersRoute()}/mfa`, + {method: 'post', body: JSON.stringify({login_id: loginId})} + ); + }; + + generateMfaSecret = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/mfa/generate`, + {method: 'post'} + ); + }; + + attachDevice = async (deviceId: string) => { + return this.doFetch( + `${this.getUsersRoute()}/sessions/device`, + {method: 'put', body: JSON.stringify({device_id: deviceId})} + ); + }; + + searchUsers = (term: string, options: any) => { + this.trackEvent('api', 'api_search_users'); + + return this.doFetch( + `${this.getUsersRoute()}/search`, + {method: 'post', body: JSON.stringify({term, ...options})} + ); + }; + + getStatusesByIds = async (userIds: string[]) => { + return this.doFetch( + `${this.getUsersRoute()}/status/ids`, + {method: 'post', body: JSON.stringify(userIds)} + ); + }; + + getStatus = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/status`, + {method: 'get'} + ); + }; + + updateStatus = async (status: UserStatus) => { + return this.doFetch( + `${this.getUserRoute(status.user_id)}/status`, + {method: 'put', body: JSON.stringify(status)} + ); + }; + + switchEmailToOAuth = async (service: string, email: string, password: string, mfaCode = '') => { + this.trackEvent('api', 'api_users_email_to_oauth'); + + return this.doFetch( + `${this.getUsersRoute()}/login/switch`, + {method: 'post', body: JSON.stringify({current_service: 'email', new_service: service, email, password, mfa_code: mfaCode})} + ); + }; + + switchOAuthToEmail = async (currentService: string, email: string, password: string) => { + this.trackEvent('api', 'api_users_oauth_to_email'); + + return this.doFetch( + `${this.getUsersRoute()}/login/switch`, + {method: 'post', body: JSON.stringify({current_service: currentService, new_service: 'email', email, new_password: password})} + ); + }; + + switchEmailToLdap = async (email: string, emailPassword: string, ldapId: string, ldapPassword: string, mfaCode = '') => { + this.trackEvent('api', 'api_users_email_to_ldap'); + + return this.doFetch( + `${this.getUsersRoute()}/login/switch`, + {method: 'post', body: JSON.stringify({current_service: 'email', new_service: 'ldap', email, password: emailPassword, ldap_id: ldapId, new_password: ldapPassword, mfa_code: mfaCode})} + ); + }; + + switchLdapToEmail = async (ldapPassword: string, email: string, emailPassword: string, mfaCode = '') => { + this.trackEvent('api', 'api_users_ldap_to_email'); + + return this.doFetch( + `${this.getUsersRoute()}/login/switch`, + {method: 'post', body: JSON.stringify({current_service: 'ldap', new_service: 'email', email, password: ldapPassword, new_password: emailPassword, mfa_code: mfaCode})} + ); + }; + + getAuthorizedOAuthApps = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/oauth/apps/authorized`, + {method: 'get'} + ); + } + + authorizeOAuthApp = async (responseType: string, clientId: string, redirectUri: string, state: string, scope: string) => { + return this.doFetch( + `${this.url}/oauth/authorize`, + {method: 'post', body: JSON.stringify({client_id: clientId, response_type: responseType, redirect_uri: redirectUri, state, scope})} + ); + } + + deauthorizeOAuthApp = async (clientId: string) => { + return this.doFetch( + `${this.url}/oauth/deauthorize`, + {method: 'post', body: JSON.stringify({client_id: clientId})} + ); + } + + createUserAccessToken = async (userId: string, description: string) => { + this.trackEvent('api', 'api_users_create_access_token'); + + return this.doFetch( + `${this.getUserRoute(userId)}/tokens`, + {method: 'post', body: JSON.stringify({description})} + ); + } + + getUserAccessToken = async (tokenId: string) => { + return this.doFetch( + `${this.getUsersRoute()}/tokens/${tokenId}`, + {method: 'get'} + ); + } + + getUserAccessTokensForUser = async (userId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getUserRoute(userId)}/tokens${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + } + + getUserAccessTokens = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getUsersRoute()}/tokens${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + } + + revokeUserAccessToken = async (tokenId: string) => { + this.trackEvent('api', 'api_users_revoke_access_token'); + + return this.doFetch( + `${this.getUsersRoute()}/tokens/revoke`, + {method: 'post', body: JSON.stringify({token_id: tokenId})} + ); + } + + disableUserAccessToken = async (tokenId: string) => { + return this.doFetch( + `${this.getUsersRoute()}/tokens/disable`, + {method: 'post', body: JSON.stringify({token_id: tokenId})} + ); + } + + enableUserAccessToken = async (tokenId: string) => { + return this.doFetch( + `${this.getUsersRoute()}/tokens/enable`, + {method: 'post', body: JSON.stringify({token_id: tokenId})} + ); + } + + // Team Routes + + createTeam = async (team: Team) => { + this.trackEvent('api', 'api_teams_create'); + + return this.doFetch( + `${this.getTeamsRoute()}`, + {method: 'post', body: JSON.stringify(team)} + ); + }; + + deleteTeam = async (teamId: string) => { + this.trackEvent('api', 'api_teams_delete'); + + return this.doFetch( + `${this.getTeamRoute(teamId)}`, + {method: 'delete'} + ); + }; + + updateTeam = async (team: Team) => { + this.trackEvent('api', 'api_teams_update_name', {team_id: team.id}); + + return this.doFetch( + `${this.getTeamRoute(team.id)}`, + {method: 'put', body: JSON.stringify(team)} + ); + }; + + patchTeam = async (team: Partial & {id: string}) => { + this.trackEvent('api', 'api_teams_patch_name', {team_id: team.id}); + + return this.doFetch( + `${this.getTeamRoute(team.id)}/patch`, + {method: 'put', body: JSON.stringify(team)} + ); + }; + + regenerateTeamInviteId = async (teamId: string) => { + this.trackEvent('api', 'api_teams_regenerate_invite_id', {team_id: teamId}); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/regenerate_invite_id`, + {method: 'post'} + ); + }; + + updateTeamScheme = async (teamId: string, schemeId: string) => { + const patch = {scheme_id: schemeId}; + + this.trackEvent('api', 'api_teams_update_scheme', {team_id: teamId, ...patch}); + + return this.doFetch( + `${this.getTeamSchemeRoute(teamId)}`, + {method: 'put', body: JSON.stringify(patch)} + ); + }; + + checkIfTeamExists = async (teamName: string) => { + return this.doFetch( + `${this.getTeamNameRoute(teamName)}/exists`, + {method: 'get'} + ); + }; + + getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false) => { + return this.doFetch( + `${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`, + {method: 'get'} + ); + }; + + searchTeams = (term: string, page?: number, perPage?: number) => { + this.trackEvent('api', 'api_search_teams'); + + return this.doFetch( + `${this.getTeamsRoute()}/search`, + {method: 'post', body: JSON.stringify({term, page, per_page: perPage})} + ); + }; + + getTeam = async (teamId: string) => { + return this.doFetch( + this.getTeamRoute(teamId), + {method: 'get'} + ); + }; + + getTeamByName = async (teamName: string) => { + this.trackEvent('api', 'api_teams_get_team_by_name'); + + return this.doFetch( + this.getTeamNameRoute(teamName), + {method: 'get'} + ); + }; + + getMyTeams = async () => { + return this.doFetch( + `${this.getUserRoute('me')}/teams`, + {method: 'get'} + ); + }; + + getTeamsForUser = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/teams`, + {method: 'get'} + ); + }; + + getMyTeamMembers = async () => { + return this.doFetch( + `${this.getUserRoute('me')}/teams/members`, + {method: 'get'} + ); + }; + + getMyTeamUnreads = async () => { + return this.doFetch( + `${this.getUserRoute('me')}/teams/unread`, + {method: 'get'} + ); + }; + + getTeamMembers = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getTeamMembersRoute(teamId)}${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getTeamMembersForUser = async (userId: string) => { + return this.doFetch( + `${this.getUserRoute(userId)}/teams/members`, + {method: 'get'} + ); + }; + + getTeamMember = async (teamId: string, userId: string) => { + return this.doFetch( + `${this.getTeamMemberRoute(teamId, userId)}`, + {method: 'get'} + ); + }; + + getTeamMembersByIds = async (teamId: string, userIds: string[]) => { + return this.doFetch( + `${this.getTeamMembersRoute(teamId)}/ids`, + {method: 'post', body: JSON.stringify(userIds)} + ); + }; + + addToTeam = async (teamId: string, userId: string) => { + this.trackEvent('api', 'api_teams_invite_members', {team_id: teamId}); + + const member = {user_id: userId, team_id: teamId}; + return this.doFetch( + `${this.getTeamMembersRoute(teamId)}`, + {method: 'post', body: JSON.stringify(member)} + ); + }; + + addToTeamFromInvite = async (token = '', inviteId = '') => { + this.trackEvent('api', 'api_teams_invite_members'); + + const query = buildQueryString({token, invite_id: inviteId}); + return this.doFetch( + `${this.getTeamsRoute()}/members/invite${query}`, + {method: 'post'} + ); + }; + + addUsersToTeam = async (teamId: string, userIds: string[]) => { + this.trackEvent('api', 'api_teams_batch_add_members', {team_id: teamId, count: userIds.length}); + + const members: any = []; + userIds.forEach((id) => members.push({team_id: teamId, user_id: id})); + return this.doFetch( + `${this.getTeamMembersRoute(teamId)}/batch`, + {method: 'post', body: JSON.stringify(members)} + ); + }; + + addUsersToTeamGracefully = async (teamId: string, userIds: string[]) => { + this.trackEvent('api', 'api_teams_batch_add_members', {team_id: teamId, count: userIds.length}); + + const members: any = []; + userIds.forEach((id) => members.push({team_id: teamId, user_id: id})); + return this.doFetch( + `${this.getTeamMembersRoute(teamId)}/batch?graceful=true`, + {method: 'post', body: JSON.stringify(members)} + ); + }; + + joinTeam = async (inviteId: string) => { + const query = buildQueryString({invite_id: inviteId}); + return this.doFetch( + `${this.getTeamsRoute()}/members/invite${query}`, + {method: 'post'} + ); + }; + + removeFromTeam = async (teamId: string, userId: string) => { + this.trackEvent('api', 'api_teams_remove_members', {team_id: teamId}); + + return this.doFetch( + `${this.getTeamMemberRoute(teamId, userId)}`, + {method: 'delete'} + ); + }; + + getTeamStats = async (teamId: string) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/stats`, + {method: 'get'} + ); + }; + + getTotalUsersStats = async () => { + return this.doFetch( + `${this.getUsersRoute()}/stats`, + {method: 'get'} + ); + }; + + invalidateAllEmailInvites = async () => { + return this.doFetch( + `${this.getTeamsRoute()}/invites/email`, + {method: 'delete'} + ); + }; + + getTeamInviteInfo = async (inviteId: string) => { + return this.doFetch( + `${this.getTeamsRoute()}/invite/${inviteId}`, + {method: 'get'} + ); + }; + + updateTeamMemberRoles = async (teamId: string, userId: string, roles: string[]) => { + this.trackEvent('api', 'api_teams_update_member_roles', {team_id: teamId}); + + return this.doFetch( + `${this.getTeamMemberRoute(teamId, userId)}/roles`, + {method: 'put', body: JSON.stringify({roles})} + ); + }; + + sendEmailInvitesToTeam = async (teamId: string, emails: string[]) => { + this.trackEvent('api', 'api_teams_invite_members', {team_id: teamId}); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/invite/email`, + {method: 'post', body: JSON.stringify(emails)} + ); + }; + + sendEmailGuestInvitesToChannels = async (teamId: string, channelIds: string[], emails: string[], message: string) => { + this.trackEvent('api', 'api_teams_invite_guests', {team_id: teamId, channel_ids: channelIds}); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/invite-guests/email`, + {method: 'post', body: JSON.stringify({emails, channels: channelIds, message})} + ); + }; + + sendEmailInvitesToTeamGracefully = async (teamId: string, emails: string[]) => { + this.trackEvent('api', 'api_teams_invite_members', {team_id: teamId}); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/invite/email?graceful=true`, + {method: 'post', body: JSON.stringify(emails)} + ); + }; + + sendEmailGuestInvitesToChannelsGracefully = async (teamId: string, channelIds: string[], emails: string[], message: string) => { + this.trackEvent('api', 'api_teams_invite_guests', {team_id: teamId, channel_ids: channelIds}); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/invite-guests/email?graceful=true`, + {method: 'post', body: JSON.stringify({emails, channels: channelIds, message})} + ); + }; + + importTeam = async (teamId: string, file: any, importFrom: string) => { + const formData = new FormData(); + formData.append('file', file, file.name); + formData.append('filesize', file.size); + formData.append('importFrom', importFrom); + + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + `${this.getTeamRoute(teamId)}/import`, + request + ); + }; + + getTeamIconUrl = (teamId: string, lastTeamIconUpdate: number) => { + const params: any = {}; + if (lastTeamIconUpdate) { + params._ = lastTeamIconUpdate; + } + + return `${this.getTeamRoute(teamId)}/image${buildQueryString(params)}`; + }; + + setTeamIcon = async (teamId: string, imageData: any) => { + this.trackEvent('api', 'api_team_set_team_icon'); + + const formData = new FormData(); + formData.append('image', imageData); + + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + `${this.getTeamRoute(teamId)}/image`, + request + ); + }; + + removeTeamIcon = async (teamId: string) => { + this.trackEvent('api', 'api_team_remove_team_icon'); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/image`, + {method: 'delete'} + ); + }; + + updateTeamMemberSchemeRoles = async (teamId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => { + const body = {scheme_user: isSchemeUser, scheme_admin: isSchemeAdmin}; + return this.doFetch( + `${this.getTeamRoute(teamId)}/members/${userId}/schemeRoles`, + {method: 'put', body: JSON.stringify(body)} + ); + }; + + // Channel Routes + + getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => { + const queryData = { + page, + per_page: perPage, + not_associated_to_group: notAssociatedToGroup, + exclude_default_channels: excludeDefaultChannels, + include_total_count: includeTotalCount, + }; + return this.doFetch( + `${this.getChannelsRoute()}${buildQueryString(queryData)}`, + {method: 'get'} + ); + }; + + createChannel = async (channel: Channel) => { + this.trackEvent('api', 'api_channels_create', {team_id: channel.team_id}); + + return this.doFetch( + `${this.getChannelsRoute()}`, + {method: 'post', body: JSON.stringify(channel)} + ); + }; + + createDirectChannel = async (userIds: string[]) => { + this.trackEvent('api', 'api_channels_create_direct'); + + return this.doFetch( + `${this.getChannelsRoute()}/direct`, + {method: 'post', body: JSON.stringify(userIds)} + ); + }; + + createGroupChannel = async (userIds: string[]) => { + this.trackEvent('api', 'api_channels_create_group'); + + return this.doFetch( + `${this.getChannelsRoute()}/group`, + {method: 'post', body: JSON.stringify(userIds)} + ); + }; + + deleteChannel = async (channelId: string) => { + this.trackEvent('api', 'api_channels_delete', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}`, + {method: 'delete'} + ); + }; + + unarchiveChannel = async (channelId: string) => { + this.trackEvent('api', 'api_channels_unarchive', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}/restore`, + {method: 'post'} + ); + }; + + updateChannel = async (channel: Channel) => { + this.trackEvent('api', 'api_channels_update', {channel_id: channel.id}); + + return this.doFetch( + `${this.getChannelRoute(channel.id)}`, + {method: 'put', body: JSON.stringify(channel)} + ); + }; + + convertChannelToPrivate = async (channelId: string) => { + this.trackEvent('api', 'api_channels_convert_to_private', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}/convert`, + {method: 'post'} + ); + }; + + updateChannelPrivacy = async (channelId: string, privacy: any) => { + this.trackEvent('api', 'api_channels_update_privacy', {channel_id: channelId, privacy}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}/privacy`, + {method: 'put', body: JSON.stringify({privacy})} + ); + }; + + patchChannel = async (channelId: string, channelPatch: Partial) => { + this.trackEvent('api', 'api_channels_patch', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}/patch`, + {method: 'put', body: JSON.stringify(channelPatch)} + ); + }; + + updateChannelNotifyProps = async (props: any) => { + this.trackEvent('api', 'api_users_update_channel_notifcations', {channel_id: props.channel_id}); + + return this.doFetch( + `${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`, + {method: 'put', body: JSON.stringify(props)} + ); + }; + + updateChannelScheme = async (channelId: string, schemeId: string) => { + const patch = {scheme_id: schemeId}; + + this.trackEvent('api', 'api_channels_update_scheme', {channel_id: channelId, ...patch}); + + return this.doFetch( + `${this.getChannelSchemeRoute(channelId)}`, + {method: 'put', body: JSON.stringify(patch)} + ); + }; + + getChannel = async (channelId: string) => { + this.trackEvent('api', 'api_channel_get', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}`, + {method: 'get'} + ); + }; + + getChannelByName = async (teamId: string, channelName: string, includeDeleted = false) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels/name/${channelName}?include_deleted=${includeDeleted}`, + {method: 'get'} + ); + }; + + getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => { + this.trackEvent('api', 'api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted}); + + return this.doFetch( + `${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`, + {method: 'get'} + ); + }; + + getChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + getArchivedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getMyChannels = async (teamId: string, includeDeleted = false) => { + return this.doFetch( + `${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({include_deleted: includeDeleted})}`, + {method: 'get'} + ); + }; + + getMyChannelMember = async (channelId: string) => { + return this.doFetch( + `${this.getChannelMemberRoute(channelId, 'me')}`, + {method: 'get'} + ); + }; + + getMyChannelMembers = async (teamId: string) => { + return this.doFetch( + `${this.getUserRoute('me')}/teams/${teamId}/channels/members`, + {method: 'get'} + ); + }; + + getChannelMembers = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getChannelMembersRoute(channelId)}${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getChannelTimezones = async (channelId: string) => { + return this.doFetch( + `${this.getChannelRoute(channelId)}/timezones`, + {method: 'get'} + ); + }; + + getChannelMember = async (channelId: string, userId: string) => { + return this.doFetch( + `${this.getChannelMemberRoute(channelId, userId)}`, + {method: 'get'} + ); + }; + + getChannelMembersByIds = async (channelId: string, userIds: string[]) => { + return this.doFetch( + `${this.getChannelMembersRoute(channelId)}/ids`, + {method: 'post', body: JSON.stringify(userIds)} + ); + }; + + addToChannel = async (userId: string, channelId: string, postRootId = '') => { + this.trackEvent('api', 'api_channels_add_member', {channel_id: channelId}); + + const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId}; + return this.doFetch( + `${this.getChannelMembersRoute(channelId)}`, + {method: 'post', body: JSON.stringify(member)} + ); + }; + + removeFromChannel = async (userId: string, channelId: string) => { + this.trackEvent('api', 'api_channels_remove_member', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelMemberRoute(channelId, userId)}`, + {method: 'delete'} + ); + }; + + updateChannelMemberRoles = async (channelId: string, userId: string, roles: string) => { + return this.doFetch( + `${this.getChannelMemberRoute(channelId, userId)}/roles`, + {method: 'put', body: JSON.stringify({roles})} + ); + }; + + getChannelStats = async (channelId: string) => { + return this.doFetch( + `${this.getChannelRoute(channelId)}/stats`, + {method: 'get'} + ); + }; + + getChannelModerations = async (channelId: string) => { + return this.doFetch( + `${this.getChannelRoute(channelId)}/moderations`, + {method: 'get'} + ); + }; + + patchChannelModerations = async (channelId: string, channelModerationsPatch: Array) => { + return this.doFetch( + `${this.getChannelRoute(channelId)}/moderations/patch`, + {method: 'put', body: JSON.stringify(channelModerationsPatch)} + ); + }; + + viewMyChannel = async (channelId: string, prevChannelId?: string) => { + const data = {channel_id: channelId, prev_channel_id: prevChannelId}; + return this.doFetch( + `${this.getChannelsRoute()}/members/me/view`, + {method: 'post', body: JSON.stringify(data)} + ); + }; + + autocompleteChannels = async (teamId: string, name: string) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`, + {method: 'get'} + ); + }; + + autocompleteChannelsForSearch = async (teamId: string, name: string) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels/search_autocomplete${buildQueryString({name})}`, + {method: 'get'} + ); + }; + + searchChannels = async (teamId: string, term: string) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels/search`, + {method: 'post', body: JSON.stringify({term})} + ); + }; + + searchArchivedChannels = async (teamId: string, term: string) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/channels/search_archived`, + {method: 'post', body: JSON.stringify({term})} + ); + }; + + searchAllChannels = async (term: string, notAssociatedToGroup = '', excludeDefaultChannels = false, page?: number, perPage?: number) => { + const body = { + term, + not_associated_to_group: notAssociatedToGroup, + exclude_default_channels: excludeDefaultChannels, + page, + per_page: perPage, + }; + return this.doFetch( + `${this.getChannelsRoute()}/search`, + {method: 'post', body: JSON.stringify(body)} + ); + }; + + searchGroupChannels = async (term: string) => { + return this.doFetch( + `${this.getChannelsRoute()}/group/search`, + {method: 'post', body: JSON.stringify({term})} + ); + }; + + updateChannelMemberSchemeRoles = async (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => { + const body = {scheme_user: isSchemeUser, scheme_admin: isSchemeAdmin}; + return this.doFetch( + `${this.getChannelRoute(channelId)}/members/${userId}/schemeRoles`, + {method: 'put', body: JSON.stringify(body)} + ); + }; + + // Post Routes + + createPost = async (post: Post) => { + this.trackEvent('api', 'api_posts_create', {channel_id: post.channel_id}); + + if (post.root_id != null && post.root_id !== '') { + this.trackEvent('api', 'api_posts_replied', {channel_id: post.channel_id}); + } + + return this.doFetch( + `${this.getPostsRoute()}`, + {method: 'post', body: JSON.stringify(post)} + ); + }; + + updatePost = async (post: Post) => { + this.trackEvent('api', 'api_posts_update', {channel_id: post.channel_id}); + + return this.doFetch( + `${this.getPostRoute(post.id)}`, + {method: 'put', body: JSON.stringify(post)} + ); + }; + + getPost = async (postId: string) => { + return this.doFetch( + `${this.getPostRoute(postId)}`, + {method: 'get'} + ); + }; + + patchPost = async (postPatch: Partial & {id: string}) => { + this.trackEvent('api', 'api_posts_patch', {channel_id: postPatch.channel_id}); + + return this.doFetch( + `${this.getPostRoute(postPatch.id)}/patch`, + {method: 'put', body: JSON.stringify(postPatch)} + ); + }; + + deletePost = async (postId: string) => { + this.trackEvent('api', 'api_posts_delete'); + + return this.doFetch( + `${this.getPostRoute(postId)}`, + {method: 'delete'} + ); + }; + + getPostThread = async (postId: string) => { + return this.doFetch( + `${this.getPostRoute(postId)}/thread`, + {method: 'get'} + ); + }; + + getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getPostsUnread = async (channelId: string, userId: string, limitAfter = DEFAULT_LIMIT_AFTER, limitBefore = DEFAULT_LIMIT_BEFORE) => { + return this.doFetch( + `${this.getUserRoute(userId)}/channels/${channelId}/posts/unread${buildQueryString({limit_after: limitAfter, limit_before: limitBefore})}`, + {method: 'get'} + ); + }; + + getPostsSince = async (channelId: string, since: number) => { + return this.doFetch( + `${this.getChannelRoute(channelId)}/posts${buildQueryString({since})}`, + {method: 'get'} + ); + }; + + getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_posts_get_before', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_posts_get_after', {channel_id: channelId}); + + return this.doFetch( + `${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getFileInfosForPost = async (postId: string) => { + return this.doFetch( + `${this.getPostRoute(postId)}/files/info`, + {method: 'get'} + ); + }; + + getFlaggedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_posts_get_flagged', {team_id: teamId}); + + return this.doFetch( + `${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getPinnedPosts = async (channelId: string) => { + this.trackEvent('api', 'api_posts_get_pinned', {channel_id: channelId}); + return this.doFetch( + `${this.getChannelRoute(channelId)}/pinned`, + {method: 'get'} + ); + }; + + markPostAsUnread = async (userId: string, postId: string) => { + this.trackEvent('api', 'api_post_set_unread_post'); + + return this.doFetch( + `${this.getUserRoute(userId)}/posts/${postId}/set_unread`, + {method: 'post'} + ); + } + + pinPost = async (postId: string) => { + this.trackEvent('api', 'api_posts_pin'); + + return this.doFetch( + `${this.getPostRoute(postId)}/pin`, + {method: 'post'} + ); + }; + + unpinPost = async (postId: string) => { + this.trackEvent('api', 'api_posts_unpin'); + + return this.doFetch( + `${this.getPostRoute(postId)}/unpin`, + {method: 'post'} + ); + }; + + addReaction = async (userId: string, postId: string, emojiName: string) => { + this.trackEvent('api', 'api_reactions_save', {post_id: postId}); + + return this.doFetch( + `${this.getReactionsRoute()}`, + {method: 'post', body: JSON.stringify({user_id: userId, post_id: postId, emoji_name: emojiName})} + ); + }; + + removeReaction = async (userId: string, postId: string, emojiName: string) => { + this.trackEvent('api', 'api_reactions_delete', {post_id: postId}); + + return this.doFetch( + `${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`, + {method: 'delete'} + ); + }; + + getReactionsForPost = async (postId: string) => { + return this.doFetch( + `${this.getPostRoute(postId)}/reactions`, + {method: 'get'} + ); + }; + + searchPostsWithParams = async (teamId: string, params: any) => { + this.trackEvent('api', 'api_posts_search', {team_id: teamId}); + + return this.doFetch( + `${this.getTeamRoute(teamId)}/posts/search`, + {method: 'post', body: JSON.stringify(params)} + ); + }; + + searchPosts = async (teamId: string, terms: string, isOrSearch: boolean) => { + return this.searchPostsWithParams(teamId, {terms, is_or_search: isOrSearch}); + }; + + getOpenGraphMetadata = async (url: string) => { + return this.doFetch( + `${this.getBaseRoute()}/opengraph`, + {method: 'post', body: JSON.stringify({url})} + ); + }; + + doPostAction = async (postId: string, actionId: string, selectedOption = '') => { + return this.doPostActionWithCookie(postId, actionId, '', selectedOption); + }; + + doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => { + if (selectedOption) { + this.trackEvent('api', 'api_interactive_messages_menu_selected'); + } else { + this.trackEvent('api', 'api_interactive_messages_button_clicked'); + } + + const msg: any = { + selected_option: selectedOption, + }; + if (actionCookie !== '') { + msg.cookie = actionCookie; + } + return this.doFetch( + `${this.getPostRoute(postId)}/actions/${encodeURIComponent(actionId)}`, + {method: 'post', body: JSON.stringify(msg)} + ); + }; + + // Files Routes + + getFileUrl(fileId: string, timestamp: number) { + let url = `${this.getFileRoute(fileId)}`; + if (timestamp) { + url += `?${timestamp}`; + } + + return url; + } + + getFileThumbnailUrl(fileId: string, timestamp: number) { + let url = `${this.getFileRoute(fileId)}/thumbnail`; + if (timestamp) { + url += `?${timestamp}`; + } + + return url; + } + + getFilePreviewUrl(fileId: string, timestamp: number) { + let url = `${this.getFileRoute(fileId)}/preview`; + if (timestamp) { + url += `?${timestamp}`; + } + + return url; + } + + uploadFile = async (fileFormData: any, formBoundary: string) => { + this.trackEvent('api', 'api_files_upload'); + const request: any = { + method: 'post', + body: fileFormData, + }; + + if (formBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formBoundary}`, + }; + } + + return this.doFetch( + `${this.getFilesRoute()}`, + request + ); + }; + + getFilePublicLink = async (fileId: string) => { + return this.doFetch( + `${this.getFileRoute(fileId)}/link`, + {method: 'get'} + ); + } + + // Preference Routes + + savePreferences = async (userId: string, preferences: PreferenceType[]) => { + return this.doFetch( + `${this.getPreferencesRoute(userId)}`, + {method: 'put', body: JSON.stringify(preferences)} + ); + }; + + getMyPreferences = async () => { + return this.doFetch( + `${this.getPreferencesRoute('me')}`, + {method: 'get'} + ); + }; + + deletePreferences = async (userId: string, preferences: PreferenceType[]) => { + return this.doFetch( + `${this.getPreferencesRoute(userId)}/delete`, + {method: 'post', body: JSON.stringify(preferences)} + ); + }; + + // General Routes + + ping = async () => { + return this.doFetch( + `${this.getBaseRoute()}/system/ping?time=${Date.now()}`, + {method: 'get'} + ); + }; + + logClientError = async (message: string, level = 'ERROR') => { + const url = `${this.getBaseRoute()}/logs`; + + if (!this.enableLogging) { + throw new ClientError(this.getUrl(), { + message: 'Logging disabled.', + url, + }); + } + + return this.doFetch( + url, + {method: 'post', body: JSON.stringify({message, level})} + ); + }; + + getClientConfigOld = async () => { + return this.doFetch( + `${this.getBaseRoute()}/config/client?format=old`, + {method: 'get'} + ); + }; + + getClientLicenseOld = async () => { + return this.doFetch( + `${this.getBaseRoute()}/license/client?format=old`, + {method: 'get'} + ); + }; + + getTranslations = async (url: string) => { + return this.doFetch( + url, + {method: 'get'} + ); + }; + + getWebSocketUrl = () => { + return `${this.getBaseRoute()}/websocket`; + } + + webrtcToken = async () => { + return this.doFetch( + `${this.getBaseRoute()}/webrtc/token`, + {method: 'get'} + ); + }; + + // Integration Routes + + createIncomingWebhook = async (hook: IncomingWebhook) => { + this.trackEvent('api', 'api_integrations_created', {team_id: hook.team_id}); + + return this.doFetch( + `${this.getIncomingHooksRoute()}`, + {method: 'post', body: JSON.stringify(hook)} + ); + }; + + getIncomingWebhook = async (hookId: string) => { + return this.doFetch( + `${this.getIncomingHookRoute(hookId)}`, + {method: 'get'} + ); + }; + + getIncomingWebhooks = async (teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + const queryParams: any = { + page, + per_page: perPage, + }; + + if (teamId) { + queryParams.team_id = teamId; + } + + return this.doFetch( + `${this.getIncomingHooksRoute()}${buildQueryString(queryParams)}`, + {method: 'get'} + ); + }; + + removeIncomingWebhook = async (hookId: string) => { + this.trackEvent('api', 'api_integrations_deleted'); + + return this.doFetch( + `${this.getIncomingHookRoute(hookId)}`, + {method: 'delete'} + ); + }; + + updateIncomingWebhook = async (hook: IncomingWebhook) => { + this.trackEvent('api', 'api_integrations_updated', {team_id: hook.team_id}); + + return this.doFetch( + `${this.getIncomingHookRoute(hook.id)}`, + {method: 'put', body: JSON.stringify(hook)} + ); + }; + + createOutgoingWebhook = async (hook: OutgoingWebhook) => { + this.trackEvent('api', 'api_integrations_created', {team_id: hook.team_id}); + + return this.doFetch( + `${this.getOutgoingHooksRoute()}`, + {method: 'post', body: JSON.stringify(hook)} + ); + }; + + getOutgoingWebhook = async (hookId: string) => { + return this.doFetch( + `${this.getOutgoingHookRoute(hookId)}`, + {method: 'get'} + ); + }; + + getOutgoingWebhooks = async (channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + const queryParams: any = { + page, + per_page: perPage, + }; + + if (channelId) { + queryParams.channel_id = channelId; + } + + if (teamId) { + queryParams.team_id = teamId; + } + + return this.doFetch( + `${this.getOutgoingHooksRoute()}${buildQueryString(queryParams)}`, + {method: 'get'} + ); + }; + + removeOutgoingWebhook = async (hookId: string) => { + this.trackEvent('api', 'api_integrations_deleted'); + + return this.doFetch( + `${this.getOutgoingHookRoute(hookId)}`, + {method: 'delete'} + ); + }; + + updateOutgoingWebhook = async (hook: OutgoingWebhook) => { + this.trackEvent('api', 'api_integrations_updated', {team_id: hook.team_id}); + + return this.doFetch( + `${this.getOutgoingHookRoute(hook.id)}`, + {method: 'put', body: JSON.stringify(hook)} + ); + }; + + regenOutgoingHookToken = async (id: string) => { + return this.doFetch( + `${this.getOutgoingHookRoute(id)}/regen_token`, + {method: 'post'} + ); + }; + + getCommandsList = async (teamId: string) => { + return this.doFetch( + `${this.getCommandsRoute()}?team_id=${teamId}`, + {method: 'get'} + ); + }; + + getAutocompleteCommandsList = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getTeamRoute(teamId)}/commands/autocomplete${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getCustomTeamCommands = async (teamId: string) => { + return this.doFetch( + `${this.getCommandsRoute()}?team_id=${teamId}&custom_only=true`, + {method: 'get'} + ); + }; + + executeCommand = async (command: Command, commandArgs = {}) => { + this.trackEvent('api', 'api_integrations_used'); + + return this.doFetch( + `${this.getCommandsRoute()}/execute`, + {method: 'post', body: JSON.stringify({command, ...commandArgs})} + ); + }; + + addCommand = async (command: Command) => { + this.trackEvent('api', 'api_integrations_created'); + + return this.doFetch( + `${this.getCommandsRoute()}`, + {method: 'post', body: JSON.stringify(command)} + ); + }; + + editCommand = async (command: Command) => { + this.trackEvent('api', 'api_integrations_created'); + + return this.doFetch( + `${this.getCommandsRoute()}/${command.id}`, + {method: 'put', body: JSON.stringify(command)} + ); + }; + + regenCommandToken = async (id: string) => { + return this.doFetch( + `${this.getCommandsRoute()}/${id}/regen_token`, + {method: 'put'} + ); + }; + + deleteCommand = async (id: string) => { + this.trackEvent('api', 'api_integrations_deleted'); + + return this.doFetch( + `${this.getCommandsRoute()}/${id}`, + {method: 'delete'} + ); + }; + + createOAuthApp = async (app: OAuthApp) => { + this.trackEvent('api', 'api_apps_register'); + + return this.doFetch( + `${this.getOAuthAppsRoute()}`, + {method: 'post', body: JSON.stringify(app)} + ); + }; + + editOAuthApp = async (app: OAuthApp) => { + return this.doFetch( + `${this.getOAuthAppsRoute()}/${app.id}`, + {method: 'put', body: JSON.stringify(app)} + ); + }; + + getOAuthApps = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getOAuthAppsRoute()}${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getOAuthApp = async (appId: string) => { + return this.doFetch( + `${this.getOAuthAppRoute(appId)}`, + {method: 'get'} + ); + }; + + getOAuthAppInfo = async (appId: string) => { + return this.doFetch( + `${this.getOAuthAppRoute(appId)}/info`, + {method: 'get'} + ); + }; + + deleteOAuthApp = async (appId: string) => { + this.trackEvent('api', 'api_apps_delete'); + + return this.doFetch( + `${this.getOAuthAppRoute(appId)}`, + {method: 'delete'} + ); + }; + + regenOAuthAppSecret = async (appId: string) => { + return this.doFetch( + `${this.getOAuthAppRoute(appId)}/regen_secret`, + {method: 'post'} + ); + }; + + submitInteractiveDialog = async (data: DialogSubmission) => { + this.trackEvent('api', 'api_interactive_messages_dialog_submitted'); + return this.doFetch( + `${this.getBaseRoute()}/actions/dialogs/submit`, + {method: 'post', body: JSON.stringify(data)}, + ); + }; + + // Emoji Routes + + createCustomEmoji = async (emoji: CustomEmoji, imageData: any) => { + this.trackEvent('api', 'api_emoji_custom_add'); + + const formData = new FormData(); + formData.append('image', imageData); + formData.append('emoji', JSON.stringify(emoji)); + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + `${this.getEmojisRoute()}`, + request + ); + }; + + getCustomEmoji = async (id: string) => { + return this.doFetch( + `${this.getEmojisRoute()}/${id}`, + {method: 'get'} + ); + }; + + getCustomEmojiByName = async (name: string) => { + return this.doFetch( + `${this.getEmojisRoute()}/name/${name}`, + {method: 'get'} + ); + }; + + getCustomEmojis = async (page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => { + return this.doFetch( + `${this.getEmojisRoute()}${buildQueryString({page, per_page: perPage, sort})}`, + {method: 'get'} + ); + }; + + deleteCustomEmoji = async (emojiId: string) => { + this.trackEvent('api', 'api_emoji_custom_delete'); + + return this.doFetch( + `${this.getEmojiRoute(emojiId)}`, + {method: 'delete'} + ); + }; + + getSystemEmojiImageUrl = (filename: string) => { + return `${this.url}/static/emoji/${filename}.png`; + }; + + getCustomEmojiImageUrl = (id: string) => { + return `${this.getEmojiRoute(id)}/image`; + }; + + searchCustomEmoji = async (term: string, options = {}) => { + return this.doFetch( + `${this.getEmojisRoute()}/search`, + {method: 'post', body: JSON.stringify({term, ...options})} + ); + }; + + autocompleteCustomEmoji = async (name: string) => { + return this.doFetch( + `${this.getEmojisRoute()}/autocomplete${buildQueryString({name})}`, + {method: 'get'} + ); + }; + + // Timezone Routes + + getTimezones = async () => { + return this.doFetch( + `${this.getTimezonesRoute()}`, + {method: 'get'} + ); + }; + + // Data Retention + + getDataRetentionPolicy = () => { + return this.doFetch( + `${this.getDataRetentionRoute()}/policy`, + {method: 'get'} + ); + }; + + // Jobs Routes + + getJob = async (id: string) => { + return this.doFetch( + `${this.getJobsRoute()}/${id}`, + {method: 'get'} + ); + }; + + getJobs = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getJobsRoute()}${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getJobsByType = async (type: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getJobsRoute()}/type/${type}${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + createJob = async (job: Job) => { + return this.doFetch( + `${this.getJobsRoute()}`, + {method: 'post', body: JSON.stringify(job)} + ); + }; + + cancelJob = async (id: string) => { + return this.doFetch( + `${this.getJobsRoute()}/${id}/cancel`, + {method: 'post'} + ); + }; + + // Admin Routes + + getLogs = async (page = 0, perPage = LOGS_PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBaseRoute()}/logs${buildQueryString({page, logs_per_page: perPage})}`, + {method: 'get'} + ); + }; + + getAudits = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBaseRoute()}/audits${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getConfig = async () => { + return this.doFetch( + `${this.getBaseRoute()}/config`, + {method: 'get'} + ); + }; + + updateConfig = async (config: Config) => { + return this.doFetch( + `${this.getBaseRoute()}/config`, + {method: 'put', body: JSON.stringify(config)} + ); + }; + + reloadConfig = async () => { + return this.doFetch( + `${this.getBaseRoute()}/config/reload`, + {method: 'post'} + ); + }; + + getEnvironmentConfig = async () => { + return this.doFetch( + `${this.getBaseRoute()}/config/environment`, + {method: 'get'} + ); + }; + + testEmail = async (config: Config) => { + return this.doFetch( + `${this.getBaseRoute()}/email/test`, + {method: 'post', body: JSON.stringify(config)} + ); + }; + + testSiteURL = async (siteURL: string) => { + return this.doFetch( + `${this.getBaseRoute()}/site_url/test`, + {method: 'post', body: JSON.stringify({site_url: siteURL})} + ); + }; + + testS3Connection = async (config: Config) => { + return this.doFetch( + `${this.getBaseRoute()}/file/s3_test`, + {method: 'post', body: JSON.stringify(config)} + ); + }; + + invalidateCaches = async () => { + return this.doFetch( + `${this.getBaseRoute()}/caches/invalidate`, + {method: 'post'} + ); + }; + + recycleDatabase = async () => { + return this.doFetch( + `${this.getBaseRoute()}/database/recycle`, + {method: 'post'} + ); + }; + + createComplianceReport = async (job: Job) => { + return this.doFetch( + `${this.getBaseRoute()}/compliance/reports`, + {method: 'post', body: JSON.stringify(job)} + ); + }; + + getComplianceReport = async (reportId: string) => { + return this.doFetch( + `${this.getBaseRoute()}/compliance/reports/${reportId}`, + {method: 'get'} + ); + }; + + getComplianceReports = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBaseRoute()}/compliance/reports${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + uploadBrandImage = async (imageData: any) => { + const formData = new FormData(); + formData.append('image', imageData); + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + `${this.getBrandRoute()}/image`, + request + ); + }; + + deleteBrandImage = async () => { + return this.doFetch( + `${this.getBrandRoute()}/image`, + {method: 'delete'} + ); + }; + + getClusterStatus = async () => { + return this.doFetch( + `${this.getBaseRoute()}/cluster/status`, + {method: 'get'} + ); + }; + + testLdap = async () => { + return this.doFetch( + `${this.getBaseRoute()}/ldap/test`, + {method: 'post'} + ); + }; + + syncLdap = async () => { + return this.doFetch( + `${this.getBaseRoute()}/ldap/sync`, + {method: 'post'} + ); + }; + + getLdapGroups = async (page = 0, perPage = PER_PAGE_DEFAULT, opts = {}) => { + const query = {page, per_page: perPage, ...opts}; + return this.doFetch( + `${this.getBaseRoute()}/ldap/groups${buildQueryString(query)}`, + {method: 'get'} + ); + }; + + linkLdapGroup = async (key: string) => { + return this.doFetch( + `${this.getBaseRoute()}/ldap/groups/${encodeURI(key)}/link`, + {method: 'post'} + ); + }; + + unlinkLdapGroup = async (key: string) => { + return this.doFetch( + `${this.getBaseRoute()}/ldap/groups/${encodeURI(key)}/link`, + {method: 'delete'} + ); + }; + + getSamlCertificateStatus = async () => { + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/status`, + {method: 'get'} + ); + }; + + uploadPublicSamlCertificate = async (fileData: any) => { + const formData = new FormData(); + formData.append('certificate', fileData); + + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/public`, + { + method: 'post', + body: formData, + } + ); + }; + + uploadPrivateSamlCertificate = async (fileData: any) => { + const formData = new FormData(); + formData.append('certificate', fileData); + + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/private`, + { + method: 'post', + body: formData, + } + ); + }; + + uploadIdpSamlCertificate = async (fileData: any) => { + const formData = new FormData(); + formData.append('certificate', fileData); + + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/idp`, + { + method: 'post', + body: formData, + } + ); + }; + + deletePublicSamlCertificate = async () => { + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/public`, + {method: 'delete'} + ); + }; + + deletePrivateSamlCertificate = async () => { + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/private`, + {method: 'delete'} + ); + }; + + deleteIdpSamlCertificate = async () => { + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/idp`, + {method: 'delete'} + ); + }; + + testElasticsearch = async (config: Config) => { + return this.doFetch( + `${this.getBaseRoute()}/elasticsearch/test`, + {method: 'post', body: JSON.stringify(config)} + ); + }; + + purgeElasticsearchIndexes = async () => { + return this.doFetch( + `${this.getBaseRoute()}/elasticsearch/purge_indexes`, + {method: 'post'} + ); + }; + + uploadLicense = async (fileData: any) => { + this.trackEvent('api', 'api_license_upload'); + + const formData = new FormData(); + formData.append('license', fileData); + + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + `${this.getBaseRoute()}/license`, + request + ); + }; + + removeLicense = async () => { + return this.doFetch( + `${this.getBaseRoute()}/license`, + {method: 'delete'} + ); + }; + + getAnalytics = async (name = 'standard', teamId = '') => { + return this.doFetch( + `${this.getBaseRoute()}/analytics/old${buildQueryString({name, team_id: teamId})}`, + {method: 'get'} + ); + }; + + // Role Routes + + getRole = async (roleId: string) => { + return this.doFetch( + `${this.getRolesRoute()}/${roleId}`, + {method: 'get'} + ); + }; + + getRoleByName = async (roleName: string) => { + return this.doFetch( + `${this.getRolesRoute()}/name/${roleName}`, + {method: 'get'} + ); + }; + + getRolesByNames = async (rolesNames: string[]) => { + return this.doFetch( + `${this.getRolesRoute()}/names`, + {method: 'post', body: JSON.stringify(rolesNames)} + ); + }; + + patchRole = async (roleId: string, rolePatch: Partial) => { + return this.doFetch( + `${this.getRolesRoute()}/${roleId}/patch`, + {method: 'put', body: JSON.stringify(rolePatch)} + ); + }; + + // Scheme Routes + + getSchemes = async (scope = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getSchemesRoute()}${buildQueryString({scope, page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + createScheme = async (scheme: Scheme) => { + this.trackEvent('api', 'api_schemes_create'); + + return this.doFetch( + `${this.getSchemesRoute()}`, + {method: 'post', body: JSON.stringify(scheme)} + ); + }; + + getScheme = async (schemeId: string) => { + return this.doFetch( + `${this.getSchemesRoute()}/${schemeId}`, + {method: 'get'} + ); + }; + + deleteScheme = async (schemeId: string) => { + this.trackEvent('api', 'api_schemes_delete'); + + return this.doFetch( + `${this.getSchemesRoute()}/${schemeId}`, + {method: 'delete'} + ); + }; + + patchScheme = async (schemeId: string, schemePatch: Partial) => { + this.trackEvent('api', 'api_schemes_patch', {scheme_id: schemeId}); + + return this.doFetch( + `${this.getSchemesRoute()}/${schemeId}/patch`, + {method: 'put', body: JSON.stringify(schemePatch)} + ); + }; + + getSchemeTeams = async (schemeId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getSchemesRoute()}/${schemeId}/teams${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getSchemeChannels = async (schemeId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getSchemesRoute()}/${schemeId}/channels${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + // Plugin Routes - EXPERIMENTAL - SUBJECT TO CHANGE + + uploadPlugin = async (fileData: any, force = false) => { + this.trackEvent('api', 'api_plugin_upload'); + + const formData = new FormData(); + if (force) { + formData.append('force', 'true'); + } + formData.append('plugin', fileData); + + const request: any = { + method: 'post', + body: formData, + }; + + if (formData.getBoundary) { + request.headers = { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }; + } + + return this.doFetch( + this.getPluginsRoute(), + request + ); + }; + + installPluginFromUrl = async (pluginDownloadUrl: string, force = false) => { + this.trackEvent('api', 'api_install_plugin'); + + const queryParams = {plugin_download_url: pluginDownloadUrl, force}; + + return this.doFetch( + `${this.getPluginsRoute()}/install_from_url${buildQueryString(queryParams)}`, + {method: 'post'} + ); + }; + + getPlugins = async () => { + return this.doFetch( + this.getPluginsRoute(), + {method: 'get'} + ); + }; + + getMarketplacePlugins = async (filter: string, localOnly = false) => { + return this.doFetch( + `${this.getPluginsMarketplaceRoute()}${buildQueryString({filter: filter || '', local_only: localOnly})}`, + {method: 'get'} + ); + } + + installMarketplacePlugin = async (id: string, version: string) => { + this.trackEvent('api', 'api_install_marketplace_plugin'); + + return this.doFetch( + `${this.getPluginsMarketplaceRoute()}`, + {method: 'post', body: JSON.stringify({id, version})} + ); + } + + getPluginStatuses = async () => { + return this.doFetch( + `${this.getPluginsRoute()}/statuses`, + {method: 'get'} + ); + }; + + removePlugin = async (pluginId: string) => { + return this.doFetch( + this.getPluginRoute(pluginId), + {method: 'delete'} + ); + }; + + getWebappPlugins = async () => { + return this.doFetch( + `${this.getPluginsRoute()}/webapp`, + {method: 'get'} + ); + }; + + enablePlugin = async (pluginId: string) => { + return this.doFetch( + `${this.getPluginRoute(pluginId)}/enable`, + {method: 'post'} + ); + }; + + disablePlugin = async (pluginId: string) => { + return this.doFetch( + `${this.getPluginRoute(pluginId)}/disable`, + {method: 'post'} + ); + }; + + // Groups + + linkGroupSyncable = async (groupID: string, syncableID: string, syncableType: string, patch: SyncablePatch) => { + return this.doFetch( + `${this.getBaseRoute()}/groups/${groupID}/${syncableType}s/${syncableID}/link`, + {method: 'post', body: JSON.stringify(patch)} + ); + }; + + unlinkGroupSyncable = async (groupID: string, syncableID: string, syncableType: string) => { + return this.doFetch( + `${this.getBaseRoute()}/groups/${groupID}/${syncableType}s/${syncableID}/link`, + {method: 'delete'} + ); + }; + + getGroupSyncables = async (groupID: string, syncableType: string) => { + return this.doFetch( + `${this.getBaseRoute()}/groups/${groupID}/${syncableType}s`, + {method: 'get'} + ); + }; + + getGroupMembers = async (groupID: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBaseRoute()}/groups/${groupID}/members${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + }; + + getGroup = async (groupID: string) => { + return this.doFetch( + `${this.getBaseRoute()}/groups/${groupID}`, + {method: 'get'} + ); + }; + + getGroupsNotAssociatedToTeam = async (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_groups_get_not_associated_to_team', {team_id: teamID}); + return this.doFetch( + `${this.getBaseRoute()}/groups${buildQueryString({not_associated_to_team: teamID, page, per_page: perPage, q, include_member_count: true})}`, + {method: 'get'} + ); + }; + + getGroupsNotAssociatedToChannel = async (channelID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_groups_get_not_associated_to_channel', {channel_id: channelID}); + return this.doFetch( + `${this.getBaseRoute()}/groups${buildQueryString({not_associated_to_channel: channelID, page, per_page: perPage, q, include_member_count: true})}`, + {method: 'get'} + ); + }; + + getGroupsAssociatedToTeam = async (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_groups_get_associated_to_team', {team_id: teamID}); + + return this.doFetch( + `${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({page, per_page: perPage, q, include_member_count: true})}`, + {method: 'get'} + ); + }; + + getGroupsAssociatedToChannel = async (channelID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => { + this.trackEvent('api', 'api_groups_get_associated_to_channel', {channel_id: channelID}); + + return this.doFetch( + `${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({page, per_page: perPage, q, include_member_count: true})}`, + {method: 'get'} + ); + }; + + getAllGroupsAssociatedToTeam = async (teamID: string) => { + return this.doFetch( + `${this.getBaseRoute()}/teams/${teamID}/groups?paginate=false`, + {method: 'get'} + ); + }; + + getAllGroupsAssociatedToChannel = async (channelID: string) => { + return this.doFetch( + `${this.getBaseRoute()}/channels/${channelID}/groups?paginate=false`, + {method: 'get'} + ); + }; + + patchGroupSyncable = async (groupID: string, syncableID: string, syncableType: string, patch: SyncablePatch) => { + return this.doFetch( + `${this.getBaseRoute()}/groups/${groupID}/${syncableType}s/${syncableID}/patch`, + {method: 'put', body: JSON.stringify(patch)} + ); + }; + + // Redirect Location + + getRedirectLocation = async (urlParam: string) => { + if (!urlParam.length) { + return Promise.resolve(); + } + const url = `${this.getRedirectLocationRoute()}${buildQueryString({url: urlParam})}`; + return this.doFetch(url, {method: 'get'}); + }; + + // Bot Routes + + createBot = async (bot: Bot) => { + return this.doFetch( + `${this.getBotsRoute()}`, + {method: 'post', body: JSON.stringify(bot)} + ); + } + + patchBot = async (botUserId: string, botPatch: BotPatch) => { + return this.doFetch( + `${this.getBotRoute(botUserId)}`, + {method: 'put', body: JSON.stringify(botPatch)} + ); + } + + getBot = async (botUserId: string) => { + return this.doFetch( + `${this.getBotRoute(botUserId)}`, + {method: 'get'} + ); + } + + getBots = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBotsRoute()}${buildQueryString({page, per_page: perPage})}`, + {method: 'get'} + ); + } + + getBotsIncludeDeleted = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBotsRoute()}${buildQueryString({include_deleted: true, page, per_page: perPage})}`, + {method: 'get'} + ); + } + + getBotsOrphaned = async (page = 0, perPage = PER_PAGE_DEFAULT) => { + return this.doFetch( + `${this.getBotsRoute()}${buildQueryString({only_orphaned: true, page, per_page: perPage})}`, + {method: 'get'} + ); + } + + disableBot = async (botUserId: string) => { + return this.doFetch( + `${this.getBotRoute(botUserId)}/disable`, + {method: 'post'} + ); + } + + enableBot = async (botUserId: string) => { + return this.doFetch( + `${this.getBotRoute(botUserId)}/enable`, + {method: 'post'} + ); + } + + assignBot = async (botUserId: string, newOwnerId: string) => { + return this.doFetch( + `${this.getBotRoute(botUserId)}/assign/${newOwnerId}`, + {method: 'post'} + ); + } + + teamMembersMinusGroupMembers = async (teamID: string, groupIDs: string[], page: number, perPage: number) => { + const query = `group_ids=${groupIDs.join(',')}&page=${page}&per_page=${perPage}`; + return this.doFetch( + `${this.getTeamRoute(teamID)}/members_minus_group_members?${query}`, + {method: 'get'}, + ); + } + + channelMembersMinusGroupMembers = async (channelID: string, groupIDs: string[], page: number, perPage: number) => { + const query = `group_ids=${groupIDs.join(',')}&page=${page}&per_page=${perPage}`; + return this.doFetch( + `${this.getChannelRoute(channelID)}/members_minus_group_members?${query}`, + {method: 'get'}, + ); + } + + getSamlMetadataFromIdp = async (samlMetadataURL: string) => { + return this.doFetch( + `${this.getBaseRoute()}/saml/metadatafromidp`, {method: 'post', body: JSON.stringify({saml_metadata_url: samlMetadataURL})} + ); + }; + + setSamlIdpCertificateFromMetadata = async (certData: string) => { + const request: any = { + method: 'post', + body: certData, + }; + + request.headers = { + 'Content-Type': 'application/x-pem-file', + }; + + return this.doFetch( + `${this.getBaseRoute()}/saml/certificate/idp`, + request + ); + }; + + // Client Helpers + + doFetch = async (url: string, options: Options) => { + const {data} = await this.doFetchWithResponse(url, options); + + return data; + }; + + doFetchWithResponse = async (url: string, options: Options) => { + const response = await fetch(url, this.getOptions(options)); + const headers = parseAndMergeNestedHeaders(response.headers); + + let data; + try { + data = await response.json(); + } catch (err) { + throw new ClientError(this.getUrl(), { + message: 'Received invalid response from the server.', + intl: { + id: 'mobile.request.invalid_response', + defaultMessage: 'Received invalid response from the server.', + }, + url, + }); + } + + if (headers.has(HEADER_X_VERSION_ID) && !headers.get('Cache-Control')) { + const serverVersion = headers.get(HEADER_X_VERSION_ID); + if (serverVersion && this.serverVersion !== serverVersion) { + this.serverVersion = serverVersion; + } + } + + if (headers.has(HEADER_X_CLUSTER_ID)) { + const clusterId = headers.get(HEADER_X_CLUSTER_ID); + if (clusterId && this.clusterId !== clusterId) { + this.clusterId = clusterId; + } + } + + if (response.ok) { + return { + response, + headers, + data, + }; + } + + const msg = data.message || ''; + + if (this.logToConsole) { + console.error(msg); // eslint-disable-line no-console + } + + throw new ClientError(this.getUrl(), { + message: msg, + server_error_id: data.id, + status_code: data.status_code, + url, + }); + }; + + trackEvent(category: string, event: string, props?: any) { + // Temporary change to allow only certain events to reduce data rate - see MM-13062 + // All events in 'admin' category are allowed, since they are low-volume + if (category !== 'admin' && ![ + 'api_posts_create', + 'api_interactive_messages_button_clicked', + 'api_interactive_messages_menu_selected', + 'api_interactive_messages_dialog_submitted', + 'ui_marketplace_download', + 'ui_marketplace_download_update', + 'ui_marketplace_configure', + 'ui_marketplace_opened', + 'ui_marketplace_closed', + 'ui_marketplace_search', + 'signup_user_01_welcome', + 'signup_select_team', + 'signup_team_01_name', + 'signup_team_02_url', + 'click_back', + 'click_signin_account', + 'click_create_account', + 'click_create_team', + 'click_system_console', + 'click_logout', + 'click_next', + 'click_finish', + 'click_dismiss_bar', + 'diagnostics_disabled', + ].includes(event)) { + return; + } + + const properties = Object.assign({ + category, + type: event, + user_actual_role: this.userRoles && isSystemAdmin(this.userRoles) ? 'system_admin, system_user' : 'system_user', + user_actual_id: this.userId, + }, props); + const options = { + context: { + ip: '0.0.0.0', + }, + page: { + path: '', + referrer: '', + search: '', + title: '', + url: '', + }, + anonymousId: '00000000000000000000000000', + }; + const globalAny: any = global; + if (globalAny && globalAny.window && globalAny.window.analytics && globalAny.window.analytics.initialized) { + globalAny.window.analytics.track('event', properties, options); + } else if (globalAny && globalAny.analytics) { + if (globalAny.analytics_context) { + options.context = globalAny.analytics_context; + } + + globalAny.analytics.track(Object.assign({ + event: 'event', + userId: this.diagnosticId, + }, {properties}, options)); + } + } +} + +function parseAndMergeNestedHeaders(originalHeaders: any) { + const headers = new Map(); + let nestedHeaders = new Map(); + originalHeaders.forEach((val: string, key: string) => { + const capitalizedKey = key.replace(/\b[a-z]/g, (l) => l.toUpperCase()); + let realVal = val; + if (val && val.match(/\n\S+:\s\S+/)) { + const nestedHeaderStrings = val.split('\n'); + realVal = nestedHeaderStrings.shift() as string; + const moreNestedHeaders = new Map( + nestedHeaderStrings.map((h: any) => h.split(/:\s/)) + ); + nestedHeaders = new Map([...nestedHeaders, ...moreNestedHeaders]); + } + headers.set(capitalizedKey, realVal); + }); + return new Map([...headers, ...nestedHeaders]); +} + +export class ClientError extends Error { + url: string; + intl: { defaultMessage: string; id: string } | { defaultMessage: string; id: string } | { id: string; defaultMessage: string; values: any } | { id: string; defaultMessage: string }; + server_error_id: any; + status_code: any; + constructor(baseUrl: string, data: any) { + super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url)); + + this.message = data.message; + this.url = data.url; + this.intl = data.intl; + this.server_error_id = data.server_error_id; + this.status_code = data.status_code; + + // Ensure message is treated as a property of this class when object spreading. Without this, + // copying the object by using `{...error}` would not include the message. + Object.defineProperty(this, 'message', {enumerable: true}); + } +} diff --git a/app/mm-redux/client/fetch_etag.ts b/app/mm-redux/client/fetch_etag.ts new file mode 100644 index 000000000..e54cca396 --- /dev/null +++ b/app/mm-redux/client/fetch_etag.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Options} from '@mm-redux/types/client4'; + +const data: {[x: string]: any} = {}; +const etags: {[x: string]: string} = {}; +export default ((url?: string, options: Options = {headers: {}}) => { + url = url || options.url; // eslint-disable-line no-param-reassign + + if (options.method === 'GET' || !options.method) { + const etag = etags[url!]; + const cachedResponse = data[`${url}${etag}`]; // ensure etag is for url + if (etag) { + options.headers!['If-None-Match'] = etag; + } + + return fetch(url!, options). + then((response) => { + if (response.status === 304) { + return cachedResponse.clone(); + } + + if (response.status === 200) { + const responseEtag = response.headers.get('Etag'); + + if (responseEtag) { + data[`${url}${responseEtag}`] = response.clone(); + etags[url!] = responseEtag; + } + } + + return response; + }); + } + + // all other requests go straight to fetch + return Reflect.apply(fetch, undefined, [url, options]); //eslint-disable-line no-undefined +}); diff --git a/app/mm-redux/client/index.ts b/app/mm-redux/client/index.ts new file mode 100644 index 000000000..f1e500cfd --- /dev/null +++ b/app/mm-redux/client/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import ClientClass4, {DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID} from './client4'; +const Client4 = new ClientClass4(); +export {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID}; diff --git a/app/mm-redux/client/websocket_client.ts b/app/mm-redux/client/websocket_client.ts new file mode 100644 index 000000000..a58f0f483 --- /dev/null +++ b/app/mm-redux/client/websocket_client.ts @@ -0,0 +1,254 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +const MAX_WEBSOCKET_FAILS = 7; +const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec + +const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins + +let Socket: any; + +class WebSocketClient { + conn?: WebSocket; + connectionUrl: null; + token: string|null; + sequence: number; + connectFailCount: number; + eventCallback?: Function; + firstConnectCallback?: Function; + reconnectCallback?: Function; + errorCallback?: Function; + closeCallback?: Function; + connectingCallback?: Function; + stop: boolean; + platform: string; + connectionTimeout: any; + + constructor() { + this.connectionUrl = null; + this.token = null; + this.sequence = 1; + this.connectFailCount = 0; + this.stop = false; + this.platform = ''; + } + + initialize(token: string|null, opts: any) { + const defaults = { + forceConnection: true, + connectionUrl: this.connectionUrl, + webSocketConnector: WebSocket, + }; + + const {connectionUrl, forceConnection, webSocketConnector, platform, ...additionalOptions} = Object.assign({}, defaults, opts); + + if (platform) { + this.platform = platform; + } + + if (forceConnection) { + this.stop = false; + } + + return new Promise((resolve, reject) => { + if (this.conn) { + resolve(); + return; + } + + if (connectionUrl == null) { + console.log('websocket must have connection url'); //eslint-disable-line no-console + reject(new Error('websocket must have connection url')); + return; + } + + if (this.connectFailCount === 0) { + console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console + } + + Socket = webSocketConnector; + if (this.connectingCallback) { + this.connectingCallback(); + } + + const regex = /^(?:https?|wss?):(?:\/\/)?[^/]*/; + const captured = (regex).exec(connectionUrl); + + let origin; + if (captured) { + origin = captured[0]; + + if (platform === 'android') { + // this is done cause for android having the port 80 or 443 will fail the connection + // the websocket will append them + const split = origin.split(':'); + const port = split[2]; + if (port === '80' || port === '443') { + origin = `${split[0]}:${split[1]}`; + } + } + } else { + // If we're unable to set the origin header, the websocket won't connect, but the URL is likely malformed anyway + const errorMessage = 'websocket failed to parse origin from ' + connectionUrl; + console.warn(errorMessage); // eslint-disable-line no-console + reject(new Error(errorMessage)); + return; + } + + this.conn = new Socket(connectionUrl, [], {headers: {origin}, ...(additionalOptions || {})}); + this.connectionUrl = connectionUrl; + this.token = token; + + this.conn!.onopen = () => { + if (token) { + // we check for the platform as a workaround until we fix on the server that further authentications + // are ignored + this.sendMessage('authentication_challenge', {token}); + } + + if (this.connectFailCount > 0) { + console.log('websocket re-established connection'); //eslint-disable-line no-console + if (this.reconnectCallback) { + this.reconnectCallback(); + } + } else if (this.firstConnectCallback) { + this.firstConnectCallback(); + } + + this.connectFailCount = 0; + resolve(); + }; + + this.conn!.onclose = () => { + this.conn = undefined; + this.sequence = 1; + + if (this.connectFailCount === 0) { + console.log('websocket closed'); //eslint-disable-line no-console + } + + this.connectFailCount++; + + if (this.closeCallback) { + this.closeCallback(this.connectFailCount); + } + + let retryTime = MIN_WEBSOCKET_RETRY_TIME; + + // If we've failed a bunch of connections then start backing off + if (this.connectFailCount > MAX_WEBSOCKET_FAILS) { + retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount; + if (retryTime > MAX_WEBSOCKET_RETRY_TIME) { + retryTime = MAX_WEBSOCKET_RETRY_TIME; + } + } + + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + } + + this.connectionTimeout = setTimeout( + () => { + if (this.stop) { + clearTimeout(this.connectionTimeout); + return; + } + this.initialize(token, opts); + }, + retryTime + ); + }; + + this.conn!.onerror = (evt) => { + if (this.connectFailCount <= 1) { + console.log('websocket error'); //eslint-disable-line no-console + console.log(evt); //eslint-disable-line no-console + } + + if (this.errorCallback) { + this.errorCallback(evt); + } + }; + + this.conn!.onmessage = (evt) => { + const msg = JSON.parse(evt.data); + if (msg.seq_reply) { + if (msg.error) { + console.warn(msg); //eslint-disable-line no-console + } + } else if (this.eventCallback) { + this.eventCallback(msg); + } + }; + }); + } + + setConnectingCallback(callback: Function) { + this.connectingCallback = callback; + } + + setEventCallback(callback: Function) { + this.eventCallback = callback; + } + + setFirstConnectCallback(callback: Function) { + this.firstConnectCallback = callback; + } + + setReconnectCallback(callback: Function) { + this.reconnectCallback = callback; + } + + setErrorCallback(callback: Function) { + this.errorCallback = callback; + } + + setCloseCallback(callback: Function) { + this.closeCallback = callback; + } + + close(stop = false) { + this.stop = stop; + this.connectFailCount = 0; + this.sequence = 1; + if (this.conn && this.conn.readyState === Socket.OPEN) { + this.conn.onclose = () => {}; //eslint-disable-line no-empty-function + this.conn.close(); + this.conn = undefined; + console.log('websocket closed'); //eslint-disable-line no-console + } + } + + sendMessage(action: string, data: any) { + const msg = { + action, + seq: this.sequence++, + data, + }; + + if (this.conn && this.conn.readyState === Socket.OPEN) { + this.conn.send(JSON.stringify(msg)); + } else if (!this.conn || this.conn.readyState === Socket.CLOSED) { + this.conn = undefined; + this.initialize(this.token, {platform: this.platform}); + } + } + + userTyping(channelId: string, parentId: string) { + this.sendMessage('user_typing', { + channel_id: channelId, + parent_id: parentId, + }); + } + + getStatuses() { + this.sendMessage('get_statuses', null); + } + + getStatusesByIds(userIds: string[]) { + this.sendMessage('get_statuses_by_ids', { + user_ids: userIds, + }); + } +} + +export default new WebSocketClient(); diff --git a/app/mm-redux/constants/channel_categories.ts b/app/mm-redux/constants/channel_categories.ts new file mode 100644 index 000000000..6b1aac9a4 --- /dev/null +++ b/app/mm-redux/constants/channel_categories.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ChannelCategoryType} from '@mm-redux/types/channel_categories'; + +export const CategoryTypes: {[name: string]: ChannelCategoryType} = { + FAVORITES: 'favorites', + PUBLIC: 'public', + PRIVATE: 'private', + DIRECT_MESSAGES: 'direct_messages', + CUSTOM: 'custom', +}; diff --git a/app/mm-redux/constants/emoji.ts b/app/mm-redux/constants/emoji.ts new file mode 100644 index 000000000..836704696 --- /dev/null +++ b/app/mm-redux/constants/emoji.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + SORT_BY_NAME: 'name', +}; diff --git a/app/mm-redux/constants/files.ts b/app/mm-redux/constants/files.ts new file mode 100644 index 000000000..0d4042479 --- /dev/null +++ b/app/mm-redux/constants/files.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Dictionary} from '@mm-redux/types/utilities'; + +const Files: Dictionary = { + AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'], + CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'], + IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg', 'tiff', 'tif'], + PATCH_TYPES: ['patch'], + PDF_TYPES: ['pdf'], + PRESENTATION_TYPES: ['ppt', 'pptx'], + SPREADSHEET_TYPES: ['xlsx', 'csv'], + TEXT_TYPES: ['txt', 'rtf'], + VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'], + WORD_TYPES: ['doc', 'docx'], +}; + +export default Files; diff --git a/app/mm-redux/constants/general.ts b/app/mm-redux/constants/general.ts new file mode 100644 index 000000000..ac79ddb4d --- /dev/null +++ b/app/mm-redux/constants/general.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + CONFIG_CHANGED: 'config_changed', + SERVER_VERSION_CHANGED: 'server_version_changed', + PAGE_SIZE_DEFAULT: 60, + PAGE_SIZE_MAXIMUM: 200, + LOGS_PAGE_SIZE_DEFAULT: 10000, + AUDITS_CHUNK_SIZE: 100, + PROFILE_CHUNK_SIZE: 100, + CHANNELS_CHUNK_SIZE: 50, + TEAMS_CHUNK_SIZE: 50, + JOBS_CHUNK_SIZE: 50, + SEARCH_TIMEOUT_MILLISECONDS: 100, + STATUS_INTERVAL: 60000, + AUTOCOMPLETE_LIMIT_DEFAULT: 25, + AUTOCOMPLETE_SPLIT_CHARACTERS: ['.', '-', '_'], + MENTION: 'mention', + OUT_OF_OFFICE: 'ooo', + OFFLINE: 'offline', + AWAY: 'away', + ONLINE: 'online', + DND: 'dnd', + PERMISSIONS_ALL: 'all', + PERMISSIONS_CHANNEL_ADMIN: 'channel_admin', + PERMISSIONS_TEAM_ADMIN: 'team_admin', + PERMISSIONS_SYSTEM_ADMIN: 'system_admin', + TEAM_GUEST_ROLE: 'team_guest', + TEAM_USER_ROLE: 'team_user', + TEAM_ADMIN_ROLE: 'team_admin', + CHANNEL_GUEST_ROLE: 'channel_guest', + CHANNEL_USER_ROLE: 'channel_user', + CHANNEL_ADMIN_ROLE: 'channel_admin', + SYSTEM_GUEST_ROLE: 'system_guest', + SYSTEM_USER_ROLE: 'system_user', + SYSTEM_ADMIN_ROLE: 'system_admin', + SYSTEM_USER_ACCESS_TOKEN_ROLE: 'system_user_access_token', + SYSTEM_POST_ALL_ROLE: 'system_post_all', + SYSTEM_POST_ALL_PUBLIC_ROLE: 'system_post_all_public', + ALLOW_EDIT_POST_ALWAYS: 'always', + ALLOW_EDIT_POST_NEVER: 'never', + ALLOW_EDIT_POST_TIME_LIMIT: 'time_limit', + DEFAULT_POST_EDIT_TIME_LIMIT: 300, + RESTRICT_DIRECT_MESSAGE_ANY: 'any', + RESTRICT_DIRECT_MESSAGE_TEAM: 'team', + SWITCH_TO_DEFAULT_CHANNEL: 'switch_to_default_channel', + DEFAULT_CHANNEL: 'town-square', + DM_CHANNEL: 'D', + OPEN_CHANNEL: 'O', + PRIVATE_CHANNEL: 'P', + GM_CHANNEL: 'G', + PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn', + PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn', + STORE_REHYDRATION_COMPLETE: 'store_hydation_complete', + OFFLINE_STORE_RESET: 'offline_store_reset', + OFFLINE_STORE_PURGE: 'offline_store_purge', + TEAMMATE_NAME_DISPLAY: { + SHOW_USERNAME: 'username', + SHOW_NICKNAME_FULLNAME: 'nickname_full_name', + SHOW_FULLNAME: 'full_name', + }, + SPECIAL_MENTIONS: ['all', 'channel', 'here'], + MAX_USERS_IN_GM: 8, + MIN_USERS_IN_GM: 3, + MAX_GROUP_CHANNELS_FOR_PROFILES: 50, + DEFAULT_LOCALE: 'en', + DEFAULT_AUTOLINKED_URL_SCHEMES: ['http', 'https', 'ftp', 'mailto', 'tel', 'mattermost'], + DISABLED: 'disabled', + DEFAULT_ON: 'default_on', + DEFAULT_OFF: 'default_off', +}; diff --git a/app/mm-redux/constants/groups.ts b/app/mm-redux/constants/groups.ts new file mode 100644 index 000000000..b66273891 --- /dev/null +++ b/app/mm-redux/constants/groups.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + SYNCABLE_TYPE_TEAM: 'team', + SYNCABLE_TYPE_CHANNEL: 'channel', +}; diff --git a/app/mm-redux/constants/index.ts b/app/mm-redux/constants/index.ts new file mode 100644 index 000000000..b8ff05fd3 --- /dev/null +++ b/app/mm-redux/constants/index.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import General from './general'; +import RequestStatus from './request_status'; +import WebsocketEvents from './websocket'; +import Preferences from './preferences'; +import Posts from './posts'; +import Files from './files'; +import Teams from './teams'; +import Stats from './stats'; +import Permissions from './permissions'; +import Emoji from './emoji'; +import Plugins from './plugins'; +import Groups from './groups'; +import Users from './users'; +import Roles from './roles'; +export {General, Preferences, Posts, Files, RequestStatus, WebsocketEvents, Teams, Stats, Permissions, Emoji, Plugins, Groups, Users, Roles}; diff --git a/app/mm-redux/constants/permissions.ts b/app/mm-redux/constants/permissions.ts new file mode 100644 index 000000000..2083bf5f3 --- /dev/null +++ b/app/mm-redux/constants/permissions.ts @@ -0,0 +1,83 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + INVITE_USER: 'invite_user', + ADD_USER_TO_TEAM: 'add_user_to_team', + USE_SLASH_COMMANDS: 'use_slash_commands', + MANAGE_SLASH_COMMANDS: 'manage_slash_commands', + MANAGE_OTHERS_SLASH_COMMANDS: 'manage_others_slash_commands', + CREATE_PUBLIC_CHANNEL: 'create_public_channel', + CREATE_PRIVATE_CHANNEL: 'create_private_channel', + MANAGE_PUBLIC_CHANNEL_MEMBERS: 'manage_public_channel_members', + MANAGE_PRIVATE_CHANNEL_MEMBERS: 'manage_private_channel_members', + ASSIGN_SYSTEM_ADMIN_ROLE: 'assign_system_admin_role', + MANAGE_ROLES: 'manage_roles', + MANAGE_TEAM_ROLES: 'manage_team_roles', + MANAGE_CHANNEL_ROLES: 'manage_channel_roles', + MANAGE_SYSTEM: 'manage_system', + CREATE_DIRECT_CHANNEL: 'create_direct_channel', + CREATE_GROUP_CHANNEL: 'create_group_channel', + MANAGE_PUBLIC_CHANNEL_PROPERTIES: 'manage_public_channel_properties', + MANAGE_PRIVATE_CHANNEL_PROPERTIES: 'manage_private_channel_properties', + LIST_PUBLIC_TEAMS: 'list_public_teams', + JOIN_PUBLIC_TEAMS: 'join_public_teams', + LIST_PRIVATE_TEAMS: 'list_private_teams', + JOIN_PRIVATE_TEAMS: 'join_private_teams', + LIST_TEAM_CHANNELS: 'list_team_channels', + JOIN_PUBLIC_CHANNELS: 'join_public_channels', + DELETE_PUBLIC_CHANNEL: 'delete_public_channel', + DELETE_PRIVATE_CHANNEL: 'delete_private_channel', + EDIT_OTHER_USERS: 'edit_other_users', + READ_CHANNEL: 'read_channel', + READ_PUBLIC_CHANNEL: 'read_public_channel', + ADD_REACTION: 'add_reaction', + REMOVE_REACTION: 'remove_reaction', + REMOVE_OTHERS_REACTIONS: 'remove_others_reactions', + PERMANENT_DELETE_USER: 'permanent_delete_user', + UPLOAD_FILE: 'upload_file', + GET_PUBLIC_LINK: 'get_public_link', + MANAGE_WEBHOOKS: 'manage_webhooks', + MANAGE_OTHERS_WEBHOOKS: 'manage_others_webhooks', + MANAGE_INCOMING_WEBHOOKS: 'manage_incoming_webhooks', + MANAGE_OTHERS_INCOMING_WEBHOOKS: 'manage_others_incoming_webhooks', + MANAGE_OUTGOING_WEBHOOKS: 'manage_outgoing_webhooks', + MANAGE_OTHERS_OUTGOING_WEBHOOKS: 'manage_others_outgoing_webhooks', + MANAGE_OAUTH: 'manage_oauth', + MANAGE_SYSTEM_WIDE_OAUTH: 'manage_system_wide_oauth', + CREATE_POST: 'create_post', + CREATE_POST_PUBLIC: 'create_post_public', + EDIT_POST: 'edit_post', + EDIT_OTHERS_POSTS: 'edit_others_posts', + DELETE_POST: 'delete_post', + DELETE_OTHERS_POSTS: 'delete_others_posts', + REMOVE_USER_FROM_TEAM: 'remove_user_from_team', + CREATE_TEAM: 'create_team', + MANAGE_TEAM: 'manage_team', + IMPORT_TEAM: 'import_team', + VIEW_TEAM: 'view_team', + LIST_USERS_WITHOUT_TEAM: 'list_users_without_team', + CREATE_USER_ACCESS_TOKEN: 'create_user_access_token', + READ_USER_ACCESS_TOKEN: 'read_user_access_token', + REVOKE_USER_ACCESS_TOKEN: 'revoke_user_access_token', + MANAGE_JOBS: 'manage_jobs', + MANAGE_EMOJIS: 'manage_emojis', + MANAGE_OTHERS_EMOJIS: 'manage_others_emojis', + CREATE_EMOJIS: 'create_emojis', + DELETE_EMOJIS: 'delete_emojis', + DELETE_OTHERS_EMOJIS: 'delete_others_emojis', + VIEW_MEMBERS: 'view_members', + INVITE_GUEST: 'invite_guest', + PROMOTE_GUEST: 'promote_guest', + DEMOTE_TO_GUEST: 'demote_to_guest', + USE_CHANNEL_MENTIONS: 'use_channel_mentions', + USE_GROUP_MENTIONS: 'use_group_mentions', + + CHANNEL_MODERATED_PERMISSIONS: { + CREATE_POST: 'create_post', + CREATE_REACTIONS: 'create_reactions', + MANAGE_MEMBERS: 'manage_members', + USE_CHANNEL_MENTIONS: 'use_channel_mentions', + }, + MANAGE_BOTS: 'manage_bots', + MANAGE_OTHERS_BOTS: 'manage_others_bots', +}; diff --git a/app/mm-redux/constants/plugins.ts b/app/mm-redux/constants/plugins.ts new file mode 100644 index 000000000..cd9031303 --- /dev/null +++ b/app/mm-redux/constants/plugins.ts @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + PLUGIN_STATE_NOT_RUNNING: 0, + PLUGIN_STATE_STARTING: 1, + PLUGIN_STATE_RUNNING: 2, + PLUGIN_STATE_FAILED_TO_START: 3, + PLUGIN_STATE_FAILED_TO_STAY_RUNNING: 4, + PLUGIN_STATE_STOPPING: 5, +}; diff --git a/app/mm-redux/constants/posts.ts b/app/mm-redux/constants/posts.ts new file mode 100644 index 000000000..921085eb1 --- /dev/null +++ b/app/mm-redux/constants/posts.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export const PostTypes = { + CHANNEL_DELETED: 'system_channel_deleted', + CHANNEL_UNARCHIVED: 'system_channel_restored', + DISPLAYNAME_CHANGE: 'system_displayname_change', + CONVERT_CHANNEL: 'system_convert_channel', + EPHEMERAL: 'system_ephemeral', + EPHEMERAL_ADD_TO_CHANNEL: 'system_ephemeral_add_to_channel', + HEADER_CHANGE: 'system_header_change', + PURPOSE_CHANGE: 'system_purpose_change', + + JOIN_LEAVE: 'system_join_leave', + JOIN_CHANNEL: 'system_join_channel', + GUEST_JOIN_CHANNEL: 'system_guest_join_channel', + LEAVE_CHANNEL: 'system_leave_channel', + ADD_REMOVE: 'system_add_remove', + ADD_TO_CHANNEL: 'system_add_to_channel', + ADD_GUEST_TO_CHANNEL: 'system_add_guest_to_chan', + REMOVE_FROM_CHANNEL: 'system_remove_from_channel', + + JOIN_TEAM: 'system_join_team', + LEAVE_TEAM: 'system_leave_team', + ADD_TO_TEAM: 'system_add_to_team', + REMOVE_FROM_TEAM: 'system_remove_from_team', + + COMBINED_USER_ACTIVITY: 'system_combined_user_activity', + ME: 'me', + ADD_BOT_TEAMS_CHANNELS: 'add_bot_teams_channels', +}; + +export default { + POST_CHUNK_SIZE: 60, + POST_DELETED: 'DELETED', + SYSTEM_MESSAGE_PREFIX: 'system_', + SYSTEM_AUTO_RESPONDER: 'system_auto_responder', + POST_TYPES: PostTypes, + MESSAGE_TYPES: { + POST: 'post', + COMMENT: 'comment', + }, + MAX_PREV_MSGS: 100, + POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5, // five minutes + IGNORE_POST_TYPES: [ + PostTypes.ADD_REMOVE, + PostTypes.ADD_TO_CHANNEL, + PostTypes.CHANNEL_DELETED, + PostTypes.CHANNEL_UNARCHIVED, + PostTypes.JOIN_LEAVE, + PostTypes.JOIN_CHANNEL, + PostTypes.LEAVE_CHANNEL, + PostTypes.REMOVE_FROM_CHANNEL, + PostTypes.JOIN_TEAM, + PostTypes.LEAVE_TEAM, + PostTypes.ADD_TO_TEAM, + PostTypes.REMOVE_FROM_TEAM, + ], + USER_ACTIVITY_POST_TYPES: [ + PostTypes.ADD_TO_CHANNEL, + PostTypes.JOIN_CHANNEL, + PostTypes.LEAVE_CHANNEL, + PostTypes.REMOVE_FROM_CHANNEL, + PostTypes.ADD_TO_TEAM, + PostTypes.JOIN_TEAM, + PostTypes.LEAVE_TEAM, + PostTypes.REMOVE_FROM_TEAM, + ], +}; diff --git a/app/mm-redux/constants/preferences.ts b/app/mm-redux/constants/preferences.ts new file mode 100644 index 000000000..e5be8b5d4 --- /dev/null +++ b/app/mm-redux/constants/preferences.ts @@ -0,0 +1,157 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Theme} from '@mm-redux/types/preferences'; +import {Dictionary} from '@mm-redux/types/utilities'; + +const Preferences: Dictionary = { + CATEGORY_CHANNEL_OPEN_TIME: 'channel_open_time', + CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time', + CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', + CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show', + CATEGORY_FLAGGED_POST: 'flagged_post', + CATEGORY_FAVORITE_CHANNEL: 'favorite_channel', + CATEGORY_AUTO_RESET_MANUAL_STATUS: 'auto_reset_manual_status', + CATEGORY_NOTIFICATIONS: 'notifications', + COMMENTS: 'comments', + COMMENTS_ANY: 'any', + COMMENTS_ROOT: 'root', + COMMENTS_NEVER: 'never', + EMAIL: 'email', + EMAIL_INTERVAL: 'email_interval', + INTERVAL_FIFTEEN_MINUTES: 15 * 60, + INTERVAL_HOUR: 60 * 60, + INTERVAL_IMMEDIATE: 30, + + // "immediate" is a 30 second interval + INTERVAL_NEVER: 0, + INTERVAL_NOT_SET: -1, + CATEGORY_DISPLAY_SETTINGS: 'display_settings', + NAME_NAME_FORMAT: 'name_format', + DISPLAY_PREFER_NICKNAME: 'nickname_full_name', + DISPLAY_PREFER_FULL_NAME: 'full_name', + DISPLAY_PREFER_USERNAME: 'username', + MENTION_KEYS: 'mention_keys', + USE_MILITARY_TIME: 'use_military_time', + CATEGORY_SIDEBAR_SETTINGS: 'sidebar_settings', + CHANNEL_SIDEBAR_ORGANIZATION: 'channel_sidebar_organization', + CHANNEL_SIDEBAR_AUTOCLOSE_DMS: 'close_unused_direct_messages', + AUTOCLOSE_DMS_ENABLED: 'after_seven_days', + CATEGORY_ADVANCED_SETTINGS: 'advanced_settings', + ADVANCED_FILTER_JOIN_LEAVE: 'join_leave', + ADVANCED_CODE_BLOCK_ON_CTRL_ENTER: 'code_block_ctrl_enter', + ADVANCED_SEND_ON_CTRL_ENTER: 'send_on_ctrl_enter', + CATEGORY_THEME: 'theme', + THEMES: { + default: { + type: 'Mattermost', + sidebarBg: '#145dbf', + sidebarText: '#ffffff', + sidebarUnreadText: '#ffffff', + sidebarTextHoverBg: '#4578bf', + sidebarTextActiveBorder: '#579eff', + sidebarTextActiveColor: '#ffffff', + sidebarHeaderBg: '#1153ab', + sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#06d6a0', + awayIndicator: '#ffbc42', + dndIndicator: '#f74343', + mentionBg: '#ffffff', + mentionBj: '#ffffff', + mentionColor: '#145dbf', + centerChannelBg: '#ffffff', + centerChannelColor: '#3d3c40', + newMessageSeparator: '#ff8800', + linkColor: '#2389d7', + buttonBg: '#166de0', + buttonColor: '#ffffff', + errorTextColor: '#fd5960', + mentionHighlightBg: '#ffe577', + mentionHighlightLink: '#166de0', + codeTheme: 'github', + } as Theme, + organization: { + type: 'Organization', + sidebarBg: '#2071a7', + sidebarText: '#ffffff', + sidebarUnreadText: '#ffffff', + sidebarTextHoverBg: '#136197', + sidebarTextActiveBorder: '#7ab0d6', + sidebarTextActiveColor: '#ffffff', + sidebarHeaderBg: '#2f81b7', + sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#7dbe00', + awayIndicator: '#dcbd4e', + dndIndicator: '#ff6a6a', + mentionBg: '#fbfbfb', + mentionBj: '#fbfbfb', + mentionColor: '#2071f7', + centerChannelBg: '#f2f4f8', + centerChannelColor: '#333333', + newMessageSeparator: '#ff8800', + linkColor: '#2f81b7', + buttonBg: '#1dacfc', + buttonColor: '#ffffff', + errorTextColor: '#a94442', + mentionHighlightBg: '#f3e197', + mentionHighlightLink: '#2f81b7', + codeTheme: 'github', + } as Theme, + mattermostDark: { + type: 'Mattermost Dark', + sidebarBg: '#1b2c3e', + sidebarText: '#ffffff', + sidebarUnreadText: '#ffffff', + sidebarTextHoverBg: '#4a5664', + sidebarTextActiveBorder: '#66b9a7', + sidebarTextActiveColor: '#ffffff', + sidebarHeaderBg: '#1b2c3e', + sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#65dcc8', + awayIndicator: '#c1b966', + dndIndicator: '#e81023', + mentionBg: '#b74a4a', + mentionBj: '#b74a4a', + mentionColor: '#ffffff', + centerChannelBg: '#2f3e4e', + centerChannelColor: '#dddddd', + newMessageSeparator: '#5de5da', + linkColor: '#a4ffeb', + buttonBg: '#4cbba4', + buttonColor: '#ffffff', + errorTextColor: '#ff6461', + mentionHighlightBg: '#984063', + mentionHighlightLink: '#a4ffeb', + codeTheme: 'solarized-dark', + } as Theme, + windows10: { + type: 'Windows Dark', + sidebarBg: '#171717', + sidebarText: '#ffffff', + sidebarUnreadText: '#ffffff', + sidebarTextHoverBg: '#302e30', + sidebarTextActiveBorder: '#196caf', + sidebarTextActiveColor: '#ffffff', + sidebarHeaderBg: '#1f1f1f', + sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#399fff', + awayIndicator: '#c1b966', + dndIndicator: '#e81023', + mentionBg: '#0177e7', + mentionBj: '#0177e7', + mentionColor: '#ffffff', + centerChannelBg: '#1f1f1f', + centerChannelColor: '#dddddd', + newMessageSeparator: '#cc992d', + linkColor: '#0d93ff', + buttonBg: '#0177e7', + buttonColor: '#ffffff', + errorTextColor: '#ff6461', + mentionHighlightBg: '#784098', + mentionHighlightLink: '#a4ffeb', + codeTheme: 'monokai', + } as Theme, + }, +}; + +export default Preferences; diff --git a/app/mm-redux/constants/request_status.ts b/app/mm-redux/constants/request_status.ts new file mode 100644 index 000000000..64ea2d096 --- /dev/null +++ b/app/mm-redux/constants/request_status.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {RequestStatusOption} from '@mm-redux/types/requests'; +import {Dictionary} from '@mm-redux/types/utilities'; + +const status: Dictionary = { + NOT_STARTED: 'not_started', + STARTED: 'started', + SUCCESS: 'success', + FAILURE: 'failure', + CANCELLED: 'cancelled', +}; + +export default status; diff --git a/app/mm-redux/constants/roles.ts b/app/mm-redux/constants/roles.ts new file mode 100644 index 000000000..6c456377c --- /dev/null +++ b/app/mm-redux/constants/roles.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export default { + MEMBERS: 'members', + GUESTS: 'guests', + ADMINS: 'admins', +}; \ No newline at end of file diff --git a/app/mm-redux/constants/schemes.ts b/app/mm-redux/constants/schemes.ts new file mode 100644 index 000000000..94544b6a1 --- /dev/null +++ b/app/mm-redux/constants/schemes.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export const ScopeTypes = { + TEAM: 'team', + CHANNEL: 'channel', +}; diff --git a/app/mm-redux/constants/stats.ts b/app/mm-redux/constants/stats.ts new file mode 100644 index 000000000..7f2d0ea02 --- /dev/null +++ b/app/mm-redux/constants/stats.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@mm-redux/utils/key_mirror'; + +export default keyMirror({ + TOTAL_USERS: null, + TOTAL_INACTIVE_USERS: null, + TOTAL_PUBLIC_CHANNELS: null, + TOTAL_PRIVATE_GROUPS: null, + TOTAL_POSTS: null, + TOTAL_TEAMS: null, + TOTAL_FILE_POSTS: null, + TOTAL_HASHTAG_POSTS: null, + TOTAL_IHOOKS: null, + TOTAL_OHOOKS: null, + TOTAL_COMMANDS: null, + TOTAL_SESSIONS: null, + POST_PER_DAY: null, + BOT_POST_PER_DAY: null, + USERS_WITH_POSTS_PER_DAY: null, + RECENTLY_ACTIVE_USERS: null, + NEWLY_CREATED_USERS: null, + TOTAL_WEBSOCKET_CONNECTIONS: null, + TOTAL_MASTER_DB_CONNECTIONS: null, + TOTAL_READ_DB_CONNECTIONS: null, + DAILY_ACTIVE_USERS: null, + MONTHLY_ACTIVE_USERS: null, +}); + diff --git a/app/mm-redux/constants/teams.ts b/app/mm-redux/constants/teams.ts new file mode 100644 index 000000000..e26ce91b1 --- /dev/null +++ b/app/mm-redux/constants/teams.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + TEAM_TYPE_OPEN: 'O', + TEAM_TYPE_INVITE: 'I', +}; diff --git a/app/mm-redux/constants/users.ts b/app/mm-redux/constants/users.ts new file mode 100644 index 000000000..1bce8032a --- /dev/null +++ b/app/mm-redux/constants/users.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + IGNORE_CHANNEL_MENTIONS_ON: 'on', + IGNORE_CHANNEL_MENTIONS_OFF: 'off', + IGNORE_CHANNEL_MENTIONS_DEFAULT: 'default', +}; diff --git a/app/mm-redux/constants/websocket.ts b/app/mm-redux/constants/websocket.ts new file mode 100644 index 000000000..7adb29c5d --- /dev/null +++ b/app/mm-redux/constants/websocket.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +const WebsocketEvents = { + POSTED: 'posted', + POST_EDITED: 'post_edited', + POST_DELETED: 'post_deleted', + POST_UNREAD: 'post_unread', + CHANNEL_CONVERTED: 'channel_converted', + CHANNEL_CREATED: 'channel_created', + CHANNEL_DELETED: 'channel_deleted', + CHANNEL_UNARCHIVED: 'channel_restored', + CHANNEL_UPDATED: 'channel_updated', + CHANNEL_VIEWED: 'channel_viewed', + CHANNEL_MEMBER_UPDATED: 'channel_member_updated', + CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated', + DIRECT_ADDED: 'direct_added', + ADDED_TO_TEAM: 'added_to_team', + LEAVE_TEAM: 'leave_team', + UPDATE_TEAM: 'update_team', + USER_ADDED: 'user_added', + USER_REMOVED: 'user_removed', + USER_UPDATED: 'user_updated', + USER_ROLE_UPDATED: 'user_role_updated', + ROLE_ADDED: 'role_added', + ROLE_REMOVED: 'role_removed', + ROLE_UPDATED: 'role_updated', + TYPING: 'typing', + STOP_TYPING: 'stop_typing', + PREFERENCE_CHANGED: 'preference_changed', + PREFERENCES_CHANGED: 'preferences_changed', + PREFERENCES_DELETED: 'preferences_deleted', + EPHEMERAL_MESSAGE: 'ephemeral_message', + STATUS_CHANGED: 'status_change', + HELLO: 'hello', + WEBRTC: 'webrtc', + REACTION_ADDED: 'reaction_added', + REACTION_REMOVED: 'reaction_removed', + EMOJI_ADDED: 'emoji_added', + LICENSE_CHANGED: 'license_changed', + CONFIG_CHANGED: 'config_changed', + PLUGIN_STATUSES_CHANGED: 'plugin_statuses_changed', + OPEN_DIALOG: 'open_dialog', + INCREASE_POST_VISIBILITY_BY_ONE: 'increase_post_visibility_by_one', +}; +export default WebsocketEvents; diff --git a/app/mm-redux/index.ts b/app/mm-redux/index.ts new file mode 100644 index 000000000..f0632bcf7 --- /dev/null +++ b/app/mm-redux/index.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as action_types from './action_types'; +import * as actions from './actions'; +import * as client from './client'; +import * as constants from './constants'; +import * as reducers from './reducers'; +import * as selectors from './selectors'; +import store from './store'; +import * as types from './types'; +import * as utils from './utils'; + +export {action_types, actions, client, constants, reducers, selectors, store, types, utils}; diff --git a/app/mm-redux/reducers/entities/bots.ts b/app/mm-redux/reducers/entities/bots.ts new file mode 100644 index 000000000..11790a1e9 --- /dev/null +++ b/app/mm-redux/reducers/entities/bots.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {BotTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Dictionary} from '@mm-redux/types/utilities'; +import {Bot} from '@mm-redux/types/bots'; + +function accounts(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case BotTypes.RECEIVED_BOT_ACCOUNTS: { + const newBots = action.data; + const nextState = {...state}; + for (const bot of newBots) { + nextState[bot.user_id] = bot; + } + return nextState; + } + case BotTypes.RECEIVED_BOT_ACCOUNT: { + const bot = action.data; + const nextState = {...state}; + nextState[bot.user_id] = bot; + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +export default combineReducers({ + accounts, +}); diff --git a/app/mm-redux/reducers/entities/channel_categories.test.js b/app/mm-redux/reducers/entities/channel_categories.test.js new file mode 100644 index 000000000..75c7eeb5d --- /dev/null +++ b/app/mm-redux/reducers/entities/channel_categories.test.js @@ -0,0 +1,156 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {CategoryTypes} from '../../constants/channel_categories'; + +import {ChannelCategoryTypes, TeamTypes} from '@mm-redux/action_types'; + +import * as Reducers from './channel_categories'; + +describe('byId', () => { + test('default categories should be added when a member is received', () => { + const initialState = {}; + + const state = Reducers.byId( + initialState, + { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, + data: { + team_id: 'team1', + }, + }, + ); + + expect(state['team1-favorites']).toBeDefined(); + expect(state['team1-public']).toBeDefined(); + expect(state['team1-private']).toBeDefined(); + expect(state['team1-direct_messages']).toBeDefined(); + }); + + test('default categories should be added when multiple members are received', () => { + const initialState = {}; + + const state = Reducers.byId( + initialState, + { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS, + data: [ + {team_id: 'team1'}, + {team_id: 'team2'}, + ], + }, + ); + + expect(state['team1-favorites']).toBeDefined(); + expect(state['team1-public']).toBeDefined(); + expect(state['team1-private']).toBeDefined(); + expect(state['team1-direct_messages']).toBeDefined(); + expect(state['team2-favorites']).toBeDefined(); + expect(state['team2-public']).toBeDefined(); + expect(state['team2-private']).toBeDefined(); + expect(state['team2-direct_messages']).toBeDefined(); + }); + + test('should remove corresponding categories when leaving a team', () => { + const initialState = { + category1: {id: 'category1', team_id: 'team1', type: CategoryTypes.CUSTOM}, + category2: {id: 'category2', team_id: 'team1', type: CategoryTypes.CUSTOM}, + dmCategory1: {id: 'dmCategory1', team_id: 'team1', type: CategoryTypes.DIRECT_MESSAGES}, + category3: {id: 'category3', team_id: 'team2', type: CategoryTypes.CUSTOM}, + category4: {id: 'category4', team_id: 'team2', type: CategoryTypes.CUSTOM}, + dmCategory2: {id: 'dmCategory1', team_id: 'team2', type: CategoryTypes.DIRECT_MESSAGES}, + }; + + const state = Reducers.byId( + initialState, + { + type: TeamTypes.LEAVE_TEAM, + data: { + id: 'team1', + }, + }, + ); + + expect(state).toEqual({ + category3: state.category3, + category4: state.category4, + dmCategory2: state.dmCategory2, + }); + }); +}); + +describe('orderByTeam', () => { + test('default category order should be added when a member is received', () => { + const initialState = {}; + + const state = Reducers.orderByTeam( + initialState, + { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, + data: { + team_id: 'team1', + }, + }, + ); + + expect(state).toEqual({ + team1: [ + 'team1-favorites', + 'team1-public', + 'team1-private', + 'team1-direct_messages', + ], + }); + }); + + test('default category order should be added when multiple members are received', () => { + const initialState = {}; + + const state = Reducers.orderByTeam( + initialState, + { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS, + data: [ + {team_id: 'team1'}, + {team_id: 'team2'}, + ], + }, + ); + + expect(state).toEqual({ + team1: [ + 'team1-favorites', + 'team1-public', + 'team1-private', + 'team1-direct_messages', + ], + team2: [ + 'team2-favorites', + 'team2-public', + 'team2-private', + 'team2-direct_messages', + ], + }); + }); + + test('should remove correspoding order when leaving a team', () => { + const initialState = { + team1: ['category1', 'category2', 'dmCategory1'], + team2: ['category3', 'category4', 'dmCategory2'], + }; + + const state = Reducers.orderByTeam( + initialState, + { + type: TeamTypes.LEAVE_TEAM, + data: { + id: 'team1', + }, + }, + ); + + expect(state).toEqual({ + team2: initialState.team2, + }); + }); +}); diff --git a/app/mm-redux/reducers/entities/channel_categories.ts b/app/mm-redux/reducers/entities/channel_categories.ts new file mode 100644 index 000000000..1f5e61124 --- /dev/null +++ b/app/mm-redux/reducers/entities/channel_categories.ts @@ -0,0 +1,187 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; + +import {CategoryTypes} from '../../constants/channel_categories'; + +import {ChannelCategoryTypes, TeamTypes, UserTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {ChannelCategory} from '@mm-redux/types/channel_categories'; +import {Team, TeamMembership} from '@mm-redux/types/teams'; +import {$ID, IDMappedObjects, RelationOneToOne} from '@mm-redux/types/utilities'; + +export function byId(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case TeamTypes.RECEIVED_MY_TEAM_MEMBER: { + // This will be removed once categories are sent by the server + const member: TeamMembership = action.data; + + // Note that this adds new categories before state to prevent overwriting existing categories + return { + ...makeDefaultCategories(member.team_id), + ...state, + }; + } + case TeamTypes.RECEIVED_MY_TEAM_MEMBERS: { + // This will be removed once categories are sent by the server + const members: TeamMembership[] = action.data; + + return members.reduce((nextState, member) => { + // Note that this adds new categories before state to prevent overwriting existing categories + return { + ...makeDefaultCategories(member.team_id), + ...nextState, + }; + }, state); + } + + // This will be added in phase 2 of Channel Sidebar Organization once the server provides the categories + // case ChannelCategoryTypes.RECEIVED_CATEGORIES: { + // const categories: ChannelCategory[] = action.data; + + // return categories.reduce((nextState, category) => { + // return { + // ...nextState, + // [category.id]: category, + // }; + // }, state); + // } + // case ChannelCategoryTypes.RECEIVED_CATEGORY: { + // const category: ChannelCategory = action.data; + + // return { + // ...state, + // [category.id]: category, + // }; + // } + + case TeamTypes.LEAVE_TEAM: { + const team: Team = action.data; + + const nextState = {...state}; + let changed = false; + + for (const category of Object.values(state)) { + if (category.team_id !== team.id) { + continue; + } + + Reflect.deleteProperty(nextState, category.id); + changed = true; + } + + if (!changed) { + return state; + } + + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +export function orderByTeam(state: RelationOneToOne[]> = {}, action: GenericAction) { + switch (action.type) { + case TeamTypes.RECEIVED_MY_TEAM_MEMBER: { + // This will be removed once categories are sent by the server + const member: TeamMembership = action.data; + + if (state[member.team_id]) { + return state; + } + + return { + ...state, + [member.team_id]: makeDefaultCategoryIds(member.team_id), + }; + } + case TeamTypes.RECEIVED_MY_TEAM_MEMBERS: { + // This will be removed once categories are sent by the server + const members: TeamMembership[] = action.data; + + return members.reduce((nextState, member) => { + if (state[member.team_id]) { + return nextState; + } + + return { + ...nextState, + [member.team_id]: makeDefaultCategoryIds(member.team_id), + }; + }, state); + } + + // This will be added in phase 2 of Channel Sidebar Organization once the server provides the categories + // case ChannelCategoryTypes.RECEIVED_CATEGORY_ORDER: { + // const teamId: string = action.data.teamId; + // const categoryIds: string[] = action.data.categoryIds; + + // return { + // ...state, + // [teamId]: categoryIds, + // }; + // } + + case TeamTypes.LEAVE_TEAM: { + const team: Team = action.data; + + if (!state[team.id]) { + return state; + } + + const nextState = {...state}; + Reflect.deleteProperty(nextState, team.id); + + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function makeDefaultCategoryIds(teamId: string): $ID[] { + return Object.keys(makeDefaultCategories(teamId)); +} + +function makeDefaultCategories(teamId: string): IDMappedObjects { + return { + [`${teamId}-favorites`]: { + id: `${teamId}-favorites`, + team_id: teamId, + type: CategoryTypes.FAVORITES, + display_name: 'Favorites', + }, + [`${teamId}-public`]: { + id: `${teamId}-public`, + team_id: teamId, + type: CategoryTypes.PUBLIC, + display_name: 'Public', + }, + [`${teamId}-private`]: { + id: `${teamId}-private`, + team_id: teamId, + type: CategoryTypes.PRIVATE, + display_name: 'Private', + }, + [`${teamId}-direct_messages`]: { + id: `${teamId}-direct_messages`, + team_id: teamId, + type: CategoryTypes.DIRECT_MESSAGES, + display_name: 'Direct Messages', + }, + }; +} + +export default combineReducers({ + byId, + orderByTeam, +}); diff --git a/app/mm-redux/reducers/entities/channels.test.js b/app/mm-redux/reducers/entities/channels.test.js new file mode 100644 index 000000000..31126bb7e --- /dev/null +++ b/app/mm-redux/reducers/entities/channels.test.js @@ -0,0 +1,591 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ChannelTypes, UserTypes} from '@mm-redux/action_types'; +import deepFreeze from '@mm-redux/utils/deep_freeze'; + +import channelsReducer, * as Reducers from './channels'; + +import {Permissions} from '../../constants'; + +describe('channels', () => { + describe('RECEIVED_CHANNEL_DELETED', () => { + test('should mark channel as deleted', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNEL_DELETED, + data: { + id: 'channel1', + deleteAt: 1000, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.channels.channel1).toEqual({ + id: 'channel1', + delete_at: 1000, + }); + expect(nextState.channels.channel2).toBe(state.channels.channel2); + }); + + test('should do nothing for a channel that is not loaded', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNEL_DELETED, + data: { + id: 'channel3', + deleteAt: 1000, + }, + }); + + expect(nextState).toBe(state); + }); + }); + + describe('RECEIVED_CHANNEL_UNARCHIVED', () => { + test('should mark channel as active', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + delete_at: 1000, + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED, + data: { + id: 'channel1', + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.channels.channel1).toEqual({ + id: 'channel1', + delete_at: 0, + }); + expect(nextState.channels.channel2).toBe(state.channels.channel2); + }); + + test('should do nothing for a channel that is not loaded', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED, + data: { + id: 'channel3', + }, + }); + + expect(nextState).toBe(state); + }); + }); + + describe('UPDATE_CHANNEL_HEADER', () => { + test('should update channel header', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + header: 'old', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.UPDATE_CHANNEL_HEADER, + data: { + channelId: 'channel1', + header: 'new', + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.channels.channel1).toEqual({ + id: 'channel1', + header: 'new', + }); + expect(nextState.channels.channel2).toBe(state.channels.channel2); + }); + + test('should do nothing for a channel that is not loaded', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + header: 'old', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.UPDATE_CHANNEL_HEADER, + data: { + channelId: 'channel3', + header: 'new', + }, + }); + + expect(nextState).toBe(state); + }); + }); + + describe('UPDATE_CHANNEL_PURPOSE', () => { + test('should update channel purpose', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + purpose: 'old', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.UPDATE_CHANNEL_PURPOSE, + data: { + channelId: 'channel1', + purpose: 'new', + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.channels.channel1).toEqual({ + id: 'channel1', + purpose: 'new', + }); + expect(nextState.channels.channel2).toBe(state.channels.channel2); + }); + + test('should do nothing for a channel that is not loaded', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + header: 'old', + }, + channel2: { + id: 'channel2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.UPDATE_CHANNEL_PURPOSE, + data: { + channelId: 'channel3', + purpose: 'new', + }, + }); + + expect(nextState).toBe(state); + }); + }); + + describe('REMOVE_MEMBER_FROM_CHANNEL', () => { + test('should remove the channel member', () => { + const state = deepFreeze({ + channels: {}, + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: { + channel1: { + memberId1: 'member-data-1', + }, + channel2: { + memberId2: 'member-data-2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.REMOVE_MEMBER_FROM_CHANNEL, + data: { + id: 'channel2', + user_id: 'memberId2', + }, + }); + + expect(nextState.membersInChannel.channel2).toEqual({}); + expect(nextState.membersInChannel.channel1).toEqual(state.membersInChannel.channel1); + }); + + test('should work when channel member doesn\'t exist', () => { + const state = deepFreeze({ + channels: {}, + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: { + channel1: { + memberId1: 'member-data-1', + }, + channel2: { + memberId2: 'member-data-2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.REMOVE_MEMBER_FROM_CHANNEL, + data: { + id: 'channel2', + user_id: 'test', + }, + }); + + expect(nextState).toEqual(state); + }); + + test('should work when channel doesn\'t exist', () => { + const state = deepFreeze({ + channels: {}, + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + manuallyUnread: {}, + membersInChannel: { + channel1: { + memberId1: 'member-data-1', + }, + channel2: { + memberId2: 'member-data-2', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.REMOVE_MEMBER_FROM_CHANNEL, + data: { + id: 'channel3', + user_id: 'memberId2', + }, + }); + + expect(nextState).toEqual(state); + }); + }); + describe('MANUALLY_UNREAD', () => { + test('should mark channel as manually unread', () => { + const state = deepFreeze({ + channel1: false, + }); + const nextState = Reducers.manuallyUnread(state, { + type: ChannelTypes.POST_UNREAD_SUCCESS, + data: {channelId: 'channel1'}, + }); + expect(nextState.channel1).toBe(true); + }); + test('should mark channel as manually unread even if undefined', () => { + const state = deepFreeze({ + }); + const nextState = Reducers.manuallyUnread(state, { + type: ChannelTypes.POST_UNREAD_SUCCESS, + data: {channelId: 'channel1'}, + }); + expect(nextState.channel1).toBe(true); + }); + test('should remove channel as manually unread', () => { + const state = deepFreeze({ + channel1: true, + }); + const nextState = Reducers.manuallyUnread(state, { + type: ChannelTypes.REMOVE_MANUALLY_UNREAD, + data: {channelId: 'channel1'}, + }); + expect(nextState.channel1).toBe(undefined); + }); + test('shouldn\'t do nothing if channel was undefined', () => { + const state = deepFreeze({ + }); + const nextState = Reducers.manuallyUnread(state, { + type: ChannelTypes.REMOVE_MANUALLY_UNREAD, + data: {channelId: 'channel1'}, + }); + expect(nextState.channel1).toBe(undefined); + }); + test('remove all marks if user logs out', () => { + const state = deepFreeze({ + channel1: true, + channel231: false, + }); + const nextState = Reducers.manuallyUnread(state, { + type: UserTypes.LOGOUT_SUCCESS, + data: {}, + }); + expect(nextState.channel1).toBe(undefined); + expect(nextState.channel231).toBe(undefined); + }); + }); + describe('RECEIVED_CHANNELS', () => { + test('should not remove current channel', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + team_id: 'team', + }, + channel2: { + id: 'channel2', + team_id: 'team', + }, + channel3: { + id: 'channel3', + team_id: 'team', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNELS, + currentChannelId: 'channel3', + teamId: 'team', + data: [{ + id: 'channel1', + team_id: 'team', + }], + }); + + expect(nextState).not.toBe(state); + expect(nextState.channels.channel1).toEqual({ + id: 'channel1', + team_id: 'team', + }); + expect(nextState.channels.channel2).toEqual({ + id: 'channel2', + team_id: 'team', + }); + expect(nextState.channels.channel3).toEqual({ + id: 'channel3', + team_id: 'team', + }); + }); + }); + + describe('RECEIVED_CHANNEL_MODERATIONS', () => { + test('Should add new channel moderations', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + team_id: 'team', + }, + }, + channelModerations: {}, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNEL_MODERATIONS, + sync: true, + currentChannelId: 'channel1', + teamId: 'team', + data: { + channelId: 'channel1', + moderations: [ + { + name: Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_POST, + roles: { + members: true, + }, + }, + ], + }, + }); + + expect(nextState.channelModerations.channel1[0].name).toEqual(Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_POST); + expect(nextState.channelModerations.channel1[0].roles.members).toEqual(true); + }); + test('Should replace existing channel moderations', () => { + const state = deepFreeze({ + channelsInTeam: {}, + currentChannelId: '', + groupsAssociatedToChannel: {}, + myMembers: {}, + stats: {}, + totalCount: 0, + membersInChannel: {}, + channels: { + channel1: { + id: 'channel1', + team_id: 'team', + }, + }, + channelModerations: { + channel1: [{ + name: Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_POST, + roles: { + members: true, + }, + }], + }, + }); + + const nextState = channelsReducer(state, { + type: ChannelTypes.RECEIVED_CHANNEL_MODERATIONS, + sync: true, + currentChannelId: 'channel1', + teamId: 'team', + data: { + channelId: 'channel1', + moderations: [ + { + name: Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_REACTIONS, + roles: { + members: true, + guests: false, + }, + }, + ], + }, + }); + + expect(nextState.channelModerations.channel1[0].name).toEqual(Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_REACTIONS); + expect(nextState.channelModerations.channel1[0].roles.members).toEqual(true); + expect(nextState.channelModerations.channel1[0].roles.guests).toEqual(false); + }); + }); +}); diff --git a/app/mm-redux/reducers/entities/channels.ts b/app/mm-redux/reducers/entities/channels.ts new file mode 100644 index 000000000..b266efb18 --- /dev/null +++ b/app/mm-redux/reducers/entities/channels.ts @@ -0,0 +1,693 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {ChannelTypes, UserTypes, SchemeTypes, GroupTypes} from '@mm-redux/action_types'; +import {General} from '../../constants'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Channel, ChannelMembership, ChannelStats} from '@mm-redux/types/channels'; +import {RelationOneToMany, RelationOneToOne, IDMappedObjects, UserIDMappedObjects} from '@mm-redux/types/utilities'; +import {Team} from '@mm-redux/types/teams'; + +function removeMemberFromChannels(state: RelationOneToOne>, action: GenericAction) { + const nextState = {...state}; + Object.keys(state).forEach((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 = {}, 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 (const channel of action.data) { + if (state[channel.id] && channel.type === General.DM_CHANNEL) { + channel.display_name = 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 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: Array = 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 = {}, 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 = {}, 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.INCREMENT_UNREAD_MSG_COUNT: { + const {channelId, amount, onlyMentions} = 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; + } + + 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} = 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: 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> = {}, 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 = {}, 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_GROUPS_ASSOCIATED_TO_CHANNEL: { + const {channelID, groups, totalGroupCount} = action.data; + const nextState = {...state}; + const associatedGroupIDs = new Set([]); + 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([]); + 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_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] = Array.from(associatedGroupIDs); + 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 = {}, 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 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, +}); diff --git a/app/mm-redux/reducers/entities/emojis.test.js b/app/mm-redux/reducers/entities/emojis.test.js new file mode 100644 index 000000000..5f534857c --- /dev/null +++ b/app/mm-redux/reducers/entities/emojis.test.js @@ -0,0 +1,259 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {PostTypes} from '@mm-redux/action_types'; +import {customEmoji as customEmojiReducer} from '@mm-redux/reducers/entities/emojis'; +import deepFreeze from '@mm-redux/utils/deep_freeze'; + +describe('reducers/entities/emojis', () => { + describe('customEmoji', () => { + const testForSinglePost = (actionType) => () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.equal(nextState, state); + }); + + it('no emojis in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: {}, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.equal(nextState, state); + }); + + it('should save custom emojis', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + }); + }); + + it('should not save custom emojis that are already loaded', () => { + const state = deepFreeze({ + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + }); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.equal(nextState, state); + }); + + it('should handle a mix of custom emojis that are and are not loaded', () => { + const state = deepFreeze({ + emoji1: {id: 'emoji1'}, + }); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + }); + }); + }; + + describe('RECEIVED_NEW_POST', testForSinglePost(PostTypes.RECEIVED_NEW_POST)); + describe('RECEIVED_POST', testForSinglePost(PostTypes.RECEIVED_POST)); + + describe('RECEIVED_POSTS', () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + }, + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.equal(nextState, state); + }); + + it('no emojis in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: {}, + }, + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.equal(nextState, state); + }); + + it('should save custom emojis', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + }); + }); + + it('should not save custom emojis that are already loaded', () => { + const state = deepFreeze({ + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + }); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.equal(nextState, state); + }); + + it('should handle a mix of custom emojis that are and are not loaded', () => { + const state = deepFreeze({ + emoji1: {id: 'emoji1'}, + }); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + }); + }); + + it('should save emojis from multiple posts', () => { + const state = deepFreeze({ + emoji1: {id: 'emoji1'}, + }); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji2'}], + }, + }, + post2: { + id: 'post2', + metadata: { + emojis: [{id: 'emoji1'}, {id: 'emoji3'}], + }, + }, + }, + }, + }; + + const nextState = customEmojiReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + emoji1: {id: 'emoji1'}, + emoji2: {id: 'emoji2'}, + emoji3: {id: 'emoji3'}, + }); + }); + }); + }); +}); diff --git a/app/mm-redux/reducers/entities/emojis.ts b/app/mm-redux/reducers/entities/emojis.ts new file mode 100644 index 000000000..d0fbb91d3 --- /dev/null +++ b/app/mm-redux/reducers/entities/emojis.ts @@ -0,0 +1,112 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {EmojiTypes, PostTypes, UserTypes} from '@mm-redux/action_types'; +import {EmojisState, CustomEmoji} from '@mm-redux/types/emojis'; +import * as types from '@mm-redux/types'; + +export function customEmoji(state: types.utilities.IDMappedObjects = {}, action: types.actions.GenericAction): types.utilities.IDMappedObjects { + switch (action.type) { + case EmojiTypes.RECEIVED_CUSTOM_EMOJI: { + const nextState = {...state}; + nextState[action.data.id] = action.data; + return nextState; + } + case EmojiTypes.RECEIVED_CUSTOM_EMOJIS: { + const nextState = {...state}; + for (const emoji of action.data) { + nextState[emoji.id] = emoji; + } + return nextState; + } + case EmojiTypes.DELETED_CUSTOM_EMOJI: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.id); + return nextState; + } + case EmojiTypes.CLEAR_CUSTOM_EMOJIS: + case UserTypes.LOGOUT_SUCCESS: + return {}; + + case PostTypes.RECEIVED_NEW_POST: + case PostTypes.RECEIVED_POST: { + const post: types.posts.Post = action.data; + + return storeEmojisForPost(state, post); + } + case PostTypes.RECEIVED_POSTS: { + const posts = Object.values(action.data.posts); + return (posts as any).reduce(storeEmojisForPost, state); // Cast to any to avoid typing problems caused by Object.values + } + + default: + return state; + } +} + +function storeEmojisForPost(state: types.utilities.IDMappedObjects, post: types.posts.Post): types.utilities.IDMappedObjects { + if (!post.metadata || !post.metadata.emojis) { + return state; + } + + return post.metadata.emojis.reduce((nextState, emoji) => { + if (nextState[emoji.id]) { + // Emoji is already in the store + return nextState; + } + + return { + ...nextState, + [emoji.id]: emoji, + }; + }, state); +} + +function nonExistentEmoji(state: Set = new Set(), action: types.actions.GenericAction): Set { + switch (action.type) { + case EmojiTypes.CUSTOM_EMOJI_DOES_NOT_EXIST: { + if (!state.has(action.data)) { + const nextState = new Set(state); + nextState.add(action.data); + return nextState; + } + return state; + } + case EmojiTypes.RECEIVED_CUSTOM_EMOJI: { + if (action.data && state.has(action.data.name)) { + const nextState = new Set(state); + nextState.delete(action.data.name); + return nextState; + } + return state; + } + case EmojiTypes.RECEIVED_CUSTOM_EMOJIS: { + const data = action.data || []; + const nextState = new Set(state); + + let changed = false; + for (const emoji of data) { + if (emoji && nextState.has(emoji.name)) { + nextState.delete(emoji.name); + changed = true; + } + } + return changed ? nextState : state; + } + case EmojiTypes.CLEAR_CUSTOM_EMOJIS: + case UserTypes.LOGOUT_SUCCESS: + return new Set(); + + default: + return state; + } +} + +export default (combineReducers({ + + // object where every key is the custom emoji id and has an object with the custom emoji details + customEmoji, + + // set containing custom emoji names that do not exist + nonExistentEmoji, +}) as (b: EmojisState, a: types.actions.GenericAction) => EmojisState); diff --git a/app/mm-redux/reducers/entities/files.test.js b/app/mm-redux/reducers/entities/files.test.js new file mode 100644 index 000000000..7afd908c2 --- /dev/null +++ b/app/mm-redux/reducers/entities/files.test.js @@ -0,0 +1,496 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {PostTypes} from '@mm-redux/action_types'; +import { + files as filesReducer, + fileIdsByPostId as fileIdsByPostIdReducer, +} from '@mm-redux/reducers/entities/files'; +import deepFreeze from '@mm-redux/utils/deep_freeze'; + +describe('reducers/entities/files', () => { + describe('files', () => { + const testForSinglePost = (actionType) => () => { + it('no post metadata attribute', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + }, + }; + + const nextState = filesReducer(state, action); + + assert.equal(nextState, state); + }); + + it('empty post metadata attribute', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + }, + metadata: {}, + }; + + const nextState = filesReducer(state, action); + + assert.equal(nextState, state); + }); + + it('no files in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + files: [], + }, + }, + }; + + const nextState = filesReducer(state, action); + + assert.equal(nextState, state); + }); + + it('should save files', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + files: [{id: 'file1', post_id: 'post'}, {id: 'file2', post_id: 'post'}], + }, + }, + }; + + const nextState = filesReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + file1: {id: 'file1', post_id: 'post'}, + file2: {id: 'file2', post_id: 'post'}, + }); + }); + }; + + describe('RECEIVED_NEW_POST', testForSinglePost(PostTypes.RECEIVED_NEW_POST)); + describe('RECEIVED_POST', testForSinglePost(PostTypes.RECEIVED_POST)); + + describe('RECEIVED_POSTS', () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + }, + }, + }, + }; + + const nextState = filesReducer(state, action); + + assert.equal(nextState, state); + }); + + it('no files in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: {}, + }, + }, + }, + }; + + const nextState = filesReducer(state, action); + + assert.equal(nextState, state); + }); + + it('should save files', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + files: [{id: 'file1', post_id: 'post'}, {id: 'file2', post_id: 'post'}], + }, + }, + }, + }, + }; + + const nextState = filesReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + file1: {id: 'file1', post_id: 'post'}, + file2: {id: 'file2', post_id: 'post'}, + }); + }); + + it('should save files for multiple posts', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + files: [{id: 'file1', post_id: 'post1'}, {id: 'file2', post_id: 'post1'}], + }, + }, + post2: { + id: 'post2', + metadata: { + files: [{id: 'file3', post_id: 'post2'}, {id: 'file4', post_id: 'post2'}], + }, + }, + }, + }, + }; + + const nextState = filesReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + file1: {id: 'file1', post_id: 'post1'}, + file2: {id: 'file2', post_id: 'post1'}, + file3: {id: 'file3', post_id: 'post2'}, + file4: {id: 'file4', post_id: 'post2'}, + }); + }); + }); + }); + + describe('fileIdsByPostId', () => { + const testForSinglePost = (actionType) => () => { + describe('no post metadata', () => { + const action = { + type: actionType, + data: { + id: 'post', + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['file1'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + }); + + describe('no files property in post metadata', () => { + const action = { + type: actionType, + data: { + id: 'post', + metadata: {}, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['file1'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + }); + + describe('empty files property in post metadata', () => { + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + files: [], + }, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: [], + }); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['file1'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: [], + }); + }); + }); + + describe('new files', () => { + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + files: [{id: 'file1', post_id: 'post'}, {id: 'file2', post_id: 'post'}], + }, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: ['file1', 'file2'], + }); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['fileOld'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: ['file1', 'file2'], + }); + }); + }); + }; + + describe('RECEIVED_NEW_POST', testForSinglePost(PostTypes.RECEIVED_NEW_POST)); + describe('RECEIVED_POST', testForSinglePost(PostTypes.RECEIVED_POST)); + + describe('RECEIVED_POSTS', () => { + describe('no post metadata', () => { + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + }, + }, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['file1'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + }); + + describe('no files property in post metadata', () => { + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: {}, + }, + }, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['file1'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.equal(nextState, state); + }); + }); + + describe('empty files property in post metadata', () => { + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + files: [], + }, + }, + }, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: [], + }); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['file1'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: [], + }); + }); + }); + + describe('new files for single post', () => { + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + files: [{id: 'file1', post_id: 'post'}, {id: 'file2', post_id: 'post'}], + }, + }, + }, + }, + }; + + it('no previous state', () => { + const state = deepFreeze({}); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: ['file1', 'file2'], + }); + }); + + it('with previous state', () => { + const state = deepFreeze({ + post: ['fileOld'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: ['file1', 'file2'], + }); + }); + }); + + describe('should save files for multiple posts', () => { + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + files: [{id: 'file1', post_id: 'post1'}, {id: 'file2', post_id: 'post1'}], + }, + }, + post2: { + id: 'post2', + metadata: { + files: [{id: 'file3', post_id: 'post2'}, {id: 'file4', post_id: 'post2'}], + }, + }, + }, + }, + }; + + it('no previous state for post1', () => { + const state = deepFreeze({ + post2: ['fileOld2'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post1: ['file1', 'file2'], + post2: ['file3', 'file4'], + }); + }); + + it('previous state for post1', () => { + const state = deepFreeze({ + post1: ['fileOld1'], + post2: ['fileOld2'], + }); + const nextState = fileIdsByPostIdReducer(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post1: ['file1', 'file2'], + post2: ['file3', 'file4'], + }); + }); + }); + }); + }); +}); diff --git a/app/mm-redux/reducers/entities/files.ts b/app/mm-redux/reducers/entities/files.ts new file mode 100644 index 000000000..7c28aab9c --- /dev/null +++ b/app/mm-redux/reducers/entities/files.ts @@ -0,0 +1,147 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {FileTypes, PostTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Post} from '@mm-redux/types/posts'; +import {FileInfo} from '@mm-redux/types/files'; +import {Dictionary} from '@mm-redux/types/utilities'; + +export function files(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case FileTypes.RECEIVED_UPLOAD_FILES: + case FileTypes.RECEIVED_FILES_FOR_POST: { + const filesById = action.data.reduce((filesMap: any, file: any) => { + return {...filesMap, + [file.id]: file, + }; + }, {} as any); + return {...state, + ...filesById, + }; + } + + case PostTypes.RECEIVED_NEW_POST: + case PostTypes.RECEIVED_POST: { + const post = action.data; + + return storeFilesForPost(state, post); + } + + case PostTypes.RECEIVED_POSTS: { + const posts = Object.values(action.data.posts); + + return posts.reduce(storeFilesForPost, state); + } + + case PostTypes.POST_DELETED: + case PostTypes.POST_REMOVED: { + if (action.data && action.data.file_ids && action.data.file_ids.length) { + const nextState = {...state}; + const fileIds = action.data.file_ids as string[]; + fileIds.forEach((id) => { + Reflect.deleteProperty(nextState, id); + }); + + return nextState; + } + + return state; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function storeFilesForPost(state: Dictionary, post: Post) { + if (!post.metadata || !post.metadata.files) { + return state; + } + + return post.metadata.files.reduce((nextState, file) => { + if (nextState[file.id]) { + // File is already in the store + return nextState; + } + + return { + ...nextState, + [file.id]: file, + }; + }, state); +} + +export function fileIdsByPostId(state: Dictionary> = {}, action: GenericAction) { + switch (action.type) { + case FileTypes.RECEIVED_FILES_FOR_POST: { + const {data, postId} = action; + const filesIdsForPost = data.map((file: FileInfo) => file.id); + return {...state, + [postId as string]: filesIdsForPost, + }; + } + + case PostTypes.RECEIVED_NEW_POST: + case PostTypes.RECEIVED_POST: { + const post = action.data; + + return storeFilesIdsForPost(state, post); + } + + case PostTypes.RECEIVED_POSTS: { + const posts = Object.values(action.data.posts); + + return posts.reduce(storeFilesIdsForPost, state); + } + + case PostTypes.POST_DELETED: + case PostTypes.POST_REMOVED: { + if (action.data) { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.id); + return nextState; + } + + return state; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function storeFilesIdsForPost(state: Dictionary, post: Post) { + if (!post.metadata || !post.metadata.files) { + return state; + } + + return { + ...state, + [post.id]: post.metadata.files ? post.metadata.files.map((file) => file.id) : [], + }; +} + +function filePublicLink(state: {link: string} = {link: ''}, action: GenericAction) { + switch (action.type) { + case FileTypes.RECEIVED_FILE_PUBLIC_LINK: { + return action.data; + } + case UserTypes.LOGOUT_SUCCESS: + return {link: ''}; + + default: + return state; + } +} + +export default combineReducers({ + files, + fileIdsByPostId, + filePublicLink, +}); diff --git a/app/mm-redux/reducers/entities/general.ts b/app/mm-redux/reducers/entities/general.ts new file mode 100644 index 000000000..a0fded765 --- /dev/null +++ b/app/mm-redux/reducers/entities/general.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {GeneralTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Config} from '@mm-redux/types/config'; + +function config(state: Partial = {}, action: GenericAction) { + switch (action.type) { + case GeneralTypes.CLIENT_CONFIG_RECEIVED: + return Object.assign({}, state, action.data); + case UserTypes.LOGIN: // Used by the mobile app + case GeneralTypes.SET_CONFIG_AND_LICENSE: + return Object.assign({}, state, action.data.config); + case GeneralTypes.CLIENT_CONFIG_RESET: + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function appState(state = false, action: GenericAction) { + switch (action.type) { + case GeneralTypes.RECEIVED_APP_STATE: + return action.data; + + default: + return state; + } +} + +function credentials(state: any = {}, action: GenericAction) { + switch (action.type) { + case GeneralTypes.RECEIVED_APP_CREDENTIALS: + return Object.assign({}, state, action.data); + + case UserTypes.LOGIN: // Used by the mobile app + return { + url: action.data.url, + }; + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function dataRetentionPolicy(state: any = {}, action: GenericAction) { + switch (action.type) { + case GeneralTypes.RECEIVED_DATA_RETENTION_POLICY: + return action.data; + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function deviceToken(state = '', action: GenericAction) { + switch (action.type) { + case GeneralTypes.RECEIVED_APP_DEVICE_TOKEN: + return action.data; + default: + return state; + } +} + +function license(state: any = {}, action: GenericAction) { + switch (action.type) { + case GeneralTypes.CLIENT_LICENSE_RECEIVED: + return action.data; + case GeneralTypes.SET_CONFIG_AND_LICENSE: + return Object.assign({}, state, action.data.license); + case GeneralTypes.CLIENT_LICENSE_RESET: + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function timezones(state: string[] = [], action: GenericAction) { + switch (action.type) { + case GeneralTypes.SUPPORTED_TIMEZONES_RECEIVED: + return action.data; + case UserTypes.LOGOUT_SUCCESS: + return []; + default: + return state; + } +} + +function serverVersion(state = '', action: GenericAction) { + switch (action.type) { + case GeneralTypes.RECEIVED_SERVER_VERSION: + return action.data; + case UserTypes.LOGOUT_SUCCESS: + return ''; + default: + return state; + } +} + +export default combineReducers({ + appState, + credentials, + config, + dataRetentionPolicy, + deviceToken, + license, + serverVersion, + timezones, +}); diff --git a/app/mm-redux/reducers/entities/gifs.ts b/app/mm-redux/reducers/entities/gifs.ts new file mode 100644 index 000000000..28d971d42 --- /dev/null +++ b/app/mm-redux/reducers/entities/gifs.ts @@ -0,0 +1,249 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {GifTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Dictionary} from '@mm-redux/types/utilities'; + +const SEARCH_SELECTORS: Dictionary = { + [GifTypes.SELECT_SEARCH_TEXT]: (state: any, action: GenericAction) => ({ + ...state, + searchText: action.searchText, + }), + [GifTypes.INVALIDATE_SEARCH_TEXT]: (state: any, action: GenericAction) => ({ + ...state, + resultsByTerm: { + ...state.resultsByTerm[action.searchText], + didInvalidate: true, + }, + }), + [GifTypes.REQUEST_SEARCH]: (state: any, action: GenericAction) => ({ + ...state, + resultsByTerm: TERM_SELECTOR[action.type](state.resultsByTerm, action), + }), + [GifTypes.RECEIVE_SEARCH]: (state: any, action: GenericAction) => ({ + ...state, + searchText: action.searchText, + resultsByTerm: TERM_SELECTOR[action.type](state.resultsByTerm, action), + }), + [GifTypes.RECEIVE_SEARCH_END]: (state: any, action: GenericAction) => ({ + ...state, + searchText: action.searchText, + resultsByTerm: TERM_SELECTOR[action.type](state.resultsByTerm, action), + }), + [GifTypes.RECEIVE_CATEGORY_SEARCH]: (state: any, action: GenericAction) => ({ + ...state, + searchText: action.searchText, + resultsByTerm: TERM_SELECTOR[action.type](state.resultsByTerm, action), + }), + [GifTypes.SEARCH_FAILURE]: (state: any, action: GenericAction) => ({ + ...state, + searchText: action.searchText, + resultsByTerm: TERM_SELECTOR[action.type](state.resultsByTerm, action), + }), + [GifTypes.CLEAR_SEARCH_RESULTS]: (state: any) => ({ + ...state, + searchText: '', + resultsByTerm: {}, + }), + [GifTypes.SAVE_SEARCH_SCROLL_POSITION]: (state: any, action: GenericAction) => ({ + ...state, + scrollPosition: action.scrollPosition, + }), + [GifTypes.SAVE_SEARCH_PRIOR_LOCATION]: (state: any, action: GenericAction) => ({ + ...state, + priorLocation: action.priorLocation, + }), + [GifTypes.UPDATE_SEARCH_TEXT]: (state: any, action: GenericAction) => ({ + ...state, + searchText: action.searchText, + }), + [GifTypes.SAVE_SEARCH_BAR_TEXT]: (state: any, action: GenericAction) => ({ + ...state, + searchBarText: action.searchBarText, + }), +}; + +const CATEGORIES_SELECTORS: Dictionary = { + [GifTypes.REQUEST_CATEGORIES_LIST]: (state: any) => ({ + ...state, + isFetching: true, + }), + [GifTypes.CATEGORIES_LIST_RECEIVED]: (state: any, action: GenericAction) => { + const {cursor, tags} = action; + const {tagsList: oldTagsList = []} = state; + const tagsDict: any = {}; + const newTagsList = tags.filter((item: any) => { + return Boolean(item && item.gfycats[0] && item.gfycats[0].width); + }).map((item: any) => { + tagsDict[item.tag] = true; + return { + tagName: item.tag, + gfyId: item.gfycats[0].gfyId, + }; + }); + const tagsList = [...oldTagsList, ...newTagsList]; + return { + ...state, + cursor, + hasMore: Boolean(cursor), + isFetching: false, + tagsList, + tagsDict, + }; + }, + [GifTypes.CATEGORIES_LIST_FAILURE]: (state: any) => ({ + ...state, + isFetching: false, + }), +}; + +const TERM_SELECTOR: Dictionary = { + [GifTypes.REQUEST_SEARCH]: (state: any, action: GenericAction) => ({ + ...state, + [action.searchText]: { + ...state[action.searchText], + isFetching: true, + didInvalidate: false, + pages: PAGE_SELECTOR[action.type](state[action.searchText], action), + }, + }), + [GifTypes.RECEIVE_SEARCH]: (state: any, action: GenericAction) => { + const gfycats = action.gfycats.filter((item: any) => { + return Boolean(item.gfyId && item.width && item.height); + }); + const newItems = gfycats.map((gfycat: any) => gfycat.gfyId); + return { + ...state, + [action.searchText]: { + ...state[action.searchText], + isFetching: false, + items: typeof state[action.searchText] !== 'undefined' && + state[action.searchText].items ? + [...state[action.searchText].items, ...newItems] : + newItems, + moreRemaining: + typeof state[action.searchText] !== 'undefined' && + state[action.searchText].items ? + [ + ...state[action.searchText].items, + ...action.gfycats, + ].length < action.found : + action.gfycats.length < action.found, + count: action.count, + found: action.found, + start: action.start, + currentPage: action.currentPage, + pages: PAGE_SELECTOR[action.type](state[action.searchText], action), + cursor: action.cursor, + }, + }; + }, + [GifTypes.RECEIVE_CATEGORY_SEARCH]: (state: any, action: GenericAction) => { + const gfycats = action.gfycats.filter((item: any) => { + return Boolean(item.gfyId && item.width && item.height); + }); + const newItems = gfycats.map((gfycat: any) => gfycat.gfyId); + return { + ...state, + [action.searchText]: { + ...state[action.searchText], + isFetching: false, + items: typeof state[action.searchText] !== 'undefined' && + state[action.searchText].items ? + [...state[action.searchText].items, ...newItems] : + newItems, + cursor: action.cursor, + moreRemaining: Boolean(action.cursor), + }, + }; + }, + [GifTypes.RECEIVE_SEARCH_END]: (state: any, action: GenericAction) => ({ + ...state, + [action.searchText]: { + ...state[action.searchText], + isFetching: false, + moreRemaining: false, + }, + }), + [GifTypes.SEARCH_FAILURE]: (state: any, action: GenericAction) => ({ + ...state, + [action.searchText]: { + ...state[action.searchText], + isFetching: false, + items: [], + moreRemaining: false, + count: 0, + found: 0, + start: 0, + isEmpty: true, + }, + }), +}; +const PAGE_SELECTOR: Dictionary = { + [GifTypes.REQUEST_SEARCH]: (state: {pages?: any} = {}) => { + if (typeof state.pages == 'undefined') { + return {}; + } + return {...state.pages}; + }, + [GifTypes.RECEIVE_SEARCH]: (state: any, action: GenericAction) => ({ + ...state.pages, + [action.currentPage]: action.gfycats.map((gfycat: any) => gfycat.gfyId), + }), +}; + +const CACHE_SELECTORS: Dictionary = { + [GifTypes.CACHE_GIFS]: (state: any, action: GenericAction) => ({ + ...state, + gifs: CACHE_GIF_SELECTOR[action.type](state.gifs, action), + updating: false, + }), + [GifTypes.CACHE_REQUEST]: (state: any, action: GenericAction) => ({ + ...state, + ...action.payload, + }), +}; + +const CACHE_GIF_SELECTOR: Dictionary = { + [GifTypes.CACHE_GIFS]: (state: any, action: GenericAction) => ({ + ...state, + ...action.gifs.reduce((map: any, obj: any) => { + map[obj.gfyId] = obj; + return map; + }, {}), + }), +}; + +function appReducer(state: any = {}, action: GenericAction) { + const nextState = {...state}; + switch (action.type) { + case GifTypes.SAVE_APP_PROPS: + return {...nextState, ...action.props}; + default: + return state; + } +} + +function categoriesReducer(state: any = {}, action: GenericAction) { + const selector = CATEGORIES_SELECTORS[action.type]; + return selector ? selector(state, action) : state; +} + +function searchReducer(state: any = {}, action: GenericAction) { + const selector = SEARCH_SELECTORS[action.type]; + return selector ? selector(state, action) : state; +} + +function cacheReducer(state: any = {}, action: GenericAction) { + const selector = CACHE_SELECTORS[action.type]; + return selector ? selector(state, action) : state; +} + +export default combineReducers({ + app: appReducer, + categories: categoriesReducer, + search: searchReducer, + cache: cacheReducer, +}); diff --git a/app/mm-redux/reducers/entities/groups.ts b/app/mm-redux/reducers/entities/groups.ts new file mode 100644 index 000000000..ae7c450f4 --- /dev/null +++ b/app/mm-redux/reducers/entities/groups.ts @@ -0,0 +1,216 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {GroupTypes} from '@mm-redux/action_types'; +import {GroupChannel, GroupSyncables, GroupTeam, Group} from '@mm-redux/types/groups'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Team, TeamMembership} from '@mm-redux/types/teams'; +import {ChannelMembership} from '@mm-redux/types/channels'; +import {Dictionary} from '@mm-redux/types/utilities'; + +function syncables(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case GroupTypes.RECEIVED_GROUP_TEAMS: { + return { + ...state, + [action.group_id]: { + ...state[action.group_id], + teams: action.data, + }, + }; + } + case GroupTypes.RECEIVED_GROUP_CHANNELS: { + return { + ...state, + [action.group_id]: { + ...state[action.group_id], + channels: action.data, + }, + }; + } + case GroupTypes.LINKED_GROUP_TEAM: { + let nextGroupTeams: GroupTeam[] = []; + + if (!state[action.data.group_id] || !state[action.data.group_id].teams) { + nextGroupTeams = [action.data]; + } else { + nextGroupTeams = {...state}[action.data.group_id].teams; + for (let i = 0, len = nextGroupTeams.length; i < len; i++) { + if (nextGroupTeams[i].team_id === action.data.team_id) { + nextGroupTeams[i] = action.data; + } + } + } + + return { + ...state, + [action.data.group_id]: { + ...state[action.data.group_id], + teams: nextGroupTeams, + }, + }; + } + case GroupTypes.LINKED_GROUP_CHANNEL: { + let nextGroupChannels: GroupChannel[] = []; + + if (!state[action.data.group_id] || !state[action.data.group_id].channels) { + nextGroupChannels = [action.data]; + } else { + nextGroupChannels = {...state}[action.data.group_id].channels; + for (let i = 0, len = nextGroupChannels.length; i < len; i++) { + if (nextGroupChannels[i].channel_id === action.data.channel_id) { + nextGroupChannels[i] = action.data; + } + } + } + + return { + ...state, + [action.data.group_id]: { + ...state[action.data.group_id], + channels: nextGroupChannels, + }, + }; + } + case GroupTypes.UNLINKED_GROUP_TEAM: { + if (!state[action.data.group_id]) { + return state; + } + const nextTeams = state[action.data.group_id].teams.slice(); + + const index = nextTeams.findIndex((groupTeam) => { + return groupTeam.team_id === action.data.syncable_id; + }); + + if (index !== -1) { + nextTeams.splice(index, 1); + } + + return { + ...state, + [action.data.group_id]: { + ...state[action.data.group_id], + teams: nextTeams, + }, + }; + } + case GroupTypes.UNLINKED_GROUP_CHANNEL: { + if (!state[action.data.group_id]) { + return state; + } + const nextChannels = state[action.data.group_id].channels.slice(); + + const index = nextChannels.findIndex((groupChannel) => { + return groupChannel.channel_id === action.data.syncable_id; + }); + + if (index !== -1) { + nextChannels.splice(index, 1); + } + + return { + ...state, + [action.data.group_id]: { + ...state[action.data.group_id], + channels: nextChannels, + }, + }; + } + case GroupTypes.PATCHED_GROUP_TEAM: { + let nextGroupTeams: GroupTeam[] = []; + + if (!state[action.data.group_id] || !state[action.data.group_id].teams) { + nextGroupTeams = [action.data]; + } else { + nextGroupTeams = {...state}[action.data.group_id].teams.slice(); + for (let i = 0, len = nextGroupTeams.length; i < len; i++) { + if (nextGroupTeams[i].team_id === action.data.team_id) { + nextGroupTeams[i] = action.data; + } + } + } + + return { + ...state, + [action.data.group_id]: { + ...state[action.data.group_id], + teams: nextGroupTeams, + }, + }; + } + case GroupTypes.PATCHED_GROUP_CHANNEL: { + let nextGroupChannels: GroupChannel[] = []; + + if (!state[action.data.group_id] || !state[action.data.group_id].channels) { + nextGroupChannels = [action.data]; + } else { + nextGroupChannels = {...state}[action.data.group_id].channels.slice(); + for (let i = 0, len = nextGroupChannels.length; i < len; i++) { + if (nextGroupChannels[i].team_id === action.data.team_id) { + nextGroupChannels[i] = action.data; + } + } + } + + return { + ...state, + [action.data.group_id]: { + ...state[action.data.group_id], + channels: nextGroupChannels, + }, + }; + } + default: + return state; + } +} + +function members(state: any = {}, action: GenericAction) { + switch (action.type) { + case GroupTypes.RECEIVED_GROUP_MEMBERS: { + return { + ...state, + [action.group_id]: { + members: action.data.members, + totalMemberCount: action.data.total_member_count, + }, + }; + } + default: + return state; + } +} + +function groups(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case GroupTypes.RECEIVED_GROUP: { + return { + ...state, + [action.data.id]: action.data, + }; + } + case GroupTypes.RECEIVED_GROUPS: { + const nextState = {...state}; + for (const group of action.data) { + nextState[group.id] = group; + } + return nextState; + } + case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM: + case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL: { + const nextState = {...state}; + for (const group of action.data.groups) { + nextState[group.id] = group; + } + return nextState; + } + default: + return state; + } +} + +export default combineReducers({ + syncables, + members, + groups, +}); diff --git a/app/mm-redux/reducers/entities/index.ts b/app/mm-redux/reducers/entities/index.ts new file mode 100644 index 000000000..06cec9205 --- /dev/null +++ b/app/mm-redux/reducers/entities/index.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; + +import channels from './channels'; +import general from './general'; +import users from './users'; +import teams from './teams'; +import posts from './posts'; +import files from './files'; +import preferences from './preferences'; +import typing from './typing'; +import integrations from './integrations'; +import emojis from './emojis'; +import gifs from './gifs'; +import jobs from './jobs'; +import search from './search'; +import roles from './roles'; +import schemes from './schemes'; +import groups from './groups'; +import bots from './bots'; +import channelCategories from './channel_categories'; + +export default combineReducers({ + general, + users, + teams, + channels, + posts, + files, + preferences, + typing, + integrations, + emojis, + gifs, + jobs, + search, + roles, + schemes, + groups, + bots, + channelCategories, +}); diff --git a/app/mm-redux/reducers/entities/integrations.ts b/app/mm-redux/reducers/entities/integrations.ts new file mode 100644 index 000000000..e0285755d --- /dev/null +++ b/app/mm-redux/reducers/entities/integrations.ts @@ -0,0 +1,237 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {IntegrationTypes, UserTypes, ChannelTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Command, IncomingWebhook, OutgoingWebhook, OAuthApp} from '@mm-redux/types/integrations'; +import {Dictionary, IDMappedObjects} from '@mm-redux/types/utilities'; + +function incomingHooks(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_INCOMING_HOOK: { + const nextState = {...state}; + nextState[action.data.id] = action.data; + return nextState; + } + case IntegrationTypes.RECEIVED_INCOMING_HOOKS: { + const nextState = {...state}; + for (const hook of action.data) { + nextState[hook.id] = hook; + } + return nextState; + } + case IntegrationTypes.DELETED_INCOMING_HOOK: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.id); + return nextState; + } + case ChannelTypes.RECEIVED_CHANNEL_DELETED: { + const nextState = {...state}; + let deleted = false; + Object.keys(nextState).forEach((id) => { + if (nextState[id].channel_id === action.data.id) { + deleted = true; + Reflect.deleteProperty(nextState, id); + } + }); + + if (deleted) { + return nextState; + } + + return state; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function outgoingHooks(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_OUTGOING_HOOK: { + const nextState = {...state}; + nextState[action.data.id] = action.data; + return nextState; + } + case IntegrationTypes.RECEIVED_OUTGOING_HOOKS: { + const nextState = {...state}; + for (const hook of action.data) { + nextState[hook.id] = hook; + } + return nextState; + } + case IntegrationTypes.DELETED_OUTGOING_HOOK: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.id); + return nextState; + } + case ChannelTypes.RECEIVED_CHANNEL_DELETED: { + const nextState = {...state}; + let deleted = false; + Object.keys(nextState).forEach((id) => { + if (nextState[id].channel_id === action.data.id) { + deleted = true; + Reflect.deleteProperty(nextState, id); + } + }); + + if (deleted) { + return nextState; + } + + return state; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function commands(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_COMMANDS: + case IntegrationTypes.RECEIVED_CUSTOM_TEAM_COMMANDS: { + const nextState = {...state}; + for (const command of action.data) { + if (command.id) { + const id = command.id; + nextState[id] = command; + } + } + + return nextState; + } + case IntegrationTypes.RECEIVED_COMMAND: + if (action.data.id) { + return { + ...state, + [action.data.id]: action.data, + }; + } + + return state; + case IntegrationTypes.RECEIVED_COMMAND_TOKEN: { + const {id, token} = action.data; + return { + ...state, + [id]: { + ...state[id], + token, + }, + }; + } + case IntegrationTypes.DELETED_COMMAND: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.id); + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function systemCommands(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_COMMANDS: { + const nextCommands: Dictionary = {}; + for (const command of action.data) { + if (!command.id) { + nextCommands[command.trigger] = command; + } + } + return nextCommands; + } + case IntegrationTypes.RECEIVED_COMMAND: + if (!action.data.id) { + return { + ...state, + [action.data.trigger]: action.data, + }; + } + + return state; + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function oauthApps(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_OAUTH_APPS: { + const nextState = {...state}; + for (const app of action.data) { + nextState[app.id] = app; + } + return nextState; + } + case IntegrationTypes.RECEIVED_OAUTH_APP: + return { + ...state, + [action.data.id]: action.data, + }; + case IntegrationTypes.DELETED_OAUTH_APP: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.id); + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function dialogTriggerId(state = '', action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID: + return action.data; + default: + return state; + } +} + +function dialog(state = '', action: GenericAction) { + switch (action.type) { + case IntegrationTypes.RECEIVED_DIALOG: + return action.data; + default: + return state; + } +} + +export default combineReducers({ + + // object where every key is the hook id and has an object with the incoming hook details + incomingHooks, + + // object where every key is the hook id and has an object with the outgoing hook details + outgoingHooks, + + // object to represent installed slash commands for a current team + commands, + + // object to represent registered oauth apps with app id as the key + oauthApps, + + // object to represent built-in slash commands + systemCommands, + + // trigger ID for interactive dialogs + dialogTriggerId, + + // data for an interactive dialog to display + dialog, +}); diff --git a/app/mm-redux/reducers/entities/jobs.ts b/app/mm-redux/reducers/entities/jobs.ts new file mode 100644 index 000000000..9fb2650ac --- /dev/null +++ b/app/mm-redux/reducers/entities/jobs.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {JobTypes} from '@mm-redux/action_types'; +import {JobsState, JobType, Job, JobsByType} from '@mm-redux/types/jobs'; +import {GenericAction} from '@mm-redux/types/actions'; +import {IDMappedObjects} from '@mm-redux/types/utilities'; + +function jobs(state: IDMappedObjects = {}, action: GenericAction): IDMappedObjects { + switch (action.type) { + case JobTypes.RECEIVED_JOB: { + const nextState = {...state}; + nextState[action.data.id] = action.data; + return nextState; + } + case JobTypes.RECEIVED_JOBS: { + const nextState = {...state}; + for (const job of action.data) { + nextState[job.id] = job; + } + return nextState; + } + default: + return state; + } +} + +function jobsByTypeList(state: JobsByType = {}, action: GenericAction): JobsByType { + switch (action.type) { + case JobTypes.RECEIVED_JOBS_BY_TYPE: { + const nextState = {...state}; + if (action.data && action.data.length && action.data.length > 0) { + nextState[action.data[0].type as JobType] = action.data; + } + return nextState; + } + default: + return state; + } +} + +export default (combineReducers({ + + // object where every key is the job id and has an object with the job details + jobs, + + // object where every key is a job type and contains a list of jobs. + jobsByTypeList, +}) as (b: JobsState, a: GenericAction) => JobsState); diff --git a/app/mm-redux/reducers/entities/posts.test.js b/app/mm-redux/reducers/entities/posts.test.js new file mode 100644 index 000000000..0bdf31fbe --- /dev/null +++ b/app/mm-redux/reducers/entities/posts.test.js @@ -0,0 +1,3961 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import expect from 'expect'; + +import { + ChannelTypes, + GeneralTypes, + PostTypes, +} from '@mm-redux/action_types'; +import {Posts} from '../../constants'; +import * as reducers from '@mm-redux/reducers/entities/posts'; +import deepFreeze from '@mm-redux/utils/deep_freeze'; + +describe('posts', () => { + for (const actionType of [ + PostTypes.RECEIVED_POST, + PostTypes.RECEIVED_NEW_POST, + ]) { + describe(`received a single post (${actionType})`, () => { + it('should add a new post', () => { + const state = deepFreeze({ + post1: {id: 'post1'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: {id: 'post2'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState).toEqual({ + post1: {id: 'post1'}, + post2: {id: 'post2'}, + }); + }); + + it('should add a new pending post', () => { + const state = deepFreeze({ + post1: {id: 'post1'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: {id: 'post2', pending_post_id: 'post2'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState).toEqual({ + post1: {id: 'post1'}, + post2: {id: 'post2', pending_post_id: 'post2'}, + }); + }); + + it('should update an existing post', () => { + const state = deepFreeze({ + post1: {id: 'post1', message: '123'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: {id: 'post1', message: 'abc'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).not.toBe(state.post1); + expect(nextState).toEqual({ + post1: {id: 'post1', message: 'abc'}, + }); + }); + + it('should remove any pending posts when receiving the actual post', () => { + const state = deepFreeze({ + pending: {id: 'pending'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: {id: 'post1', pending_post_id: 'pending'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + post1: {id: 'post1', pending_post_id: 'pending'}, + }); + }); + }); + } + + describe('received multiple posts', () => { + it('should do nothing when post list is empty', () => { + const state = deepFreeze({ + post1: {id: 'post1'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.RECEIVED_POSTS, + data: { + order: [], + posts: {}, + }, + }); + + expect(nextState).toBe(state); + }); + + it('should add new posts', () => { + const state = deepFreeze({ + post1: {id: 'post1'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.RECEIVED_POSTS, + data: { + order: ['post2', 'post3'], + posts: { + post2: {id: 'post2'}, + post3: {id: 'post3'}, + }, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState).toEqual({ + post1: {id: 'post1'}, + post2: {id: 'post2'}, + post3: {id: 'post3'}, + }); + }); + + it('should update existing posts unless we have a more recent version', () => { + const state = deepFreeze({ + post1: {id: 'post1', message: '123', update_at: 1000}, + post2: {id: 'post2', message: '456', update_at: 1000}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.RECEIVED_POSTS, + data: { + order: ['post1', 'post2'], + posts: { + post1: {id: 'post1', message: 'abc', update_at: 2000}, + post2: {id: 'post2', message: 'def', update_at: 500}, + }, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).not.toBe(state.post1); + expect(nextState.post2).toBe(state.post2); + expect(nextState).toEqual({ + post1: {id: 'post1', message: 'abc', update_at: 2000}, + post2: {id: 'post2', message: '456', update_at: 1000}, + }); + }); + + it('should set state for deleted posts', () => { + const state = deepFreeze({ + post1: {id: 'post1', message: '123', delete_at: 0, file_ids: ['file']}, + post2: {id: 'post2', message: '456', delete_at: 0, has_reactions: true}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.RECEIVED_POSTS, + data: { + order: ['post1', 'post2'], + posts: { + post1: {id: 'post1', message: '123', delete_at: 2000, file_ids: ['file']}, + post2: {id: 'post2', message: '456', delete_at: 500, has_reactions: true}, + }, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).not.toBe(state.post1); + expect(nextState.post2).not.toBe(state.post2); + expect(nextState).toEqual({ + post1: {id: 'post1', message: '123', delete_at: 2000, file_ids: [], has_reactions: false, state: Posts.POST_DELETED}, + post2: {id: 'post2', message: '456', delete_at: 500, file_ids: [], has_reactions: false, state: Posts.POST_DELETED}, + }); + }); + + it('should remove any pending posts when receiving the actual post', () => { + const state = deepFreeze({ + pending1: {id: 'pending1'}, + pending2: {id: 'pending2'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.RECEIVED_POSTS, + data: { + order: ['post1', 'post2'], + posts: { + post1: {id: 'post1', pending_post_id: 'pending1'}, + post2: {id: 'post2', pending_post_id: 'pending2'}, + }, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + post1: {id: 'post1', pending_post_id: 'pending1'}, + post2: {id: 'post2', pending_post_id: 'pending2'}, + }); + }); + + it('should not add channelId entity to postsInChannel if there were no posts in channel and it has receivedNewPosts on action', () => { + const state = deepFreeze({ + posts: {}, + postsInChannel: {}, + }); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + order: ['postId'], + posts: { + postId: { + id: 'postId', + }, + }, + }, + channelId: 'channelId', + receivedNewPosts: true, + }; + + const nextState = reducers.handlePosts(state, action); + + assert.deepEqual(nextState.postsInChannel, {}); + }); + }); + + describe(`deleting a post (${PostTypes.POST_DELETED})`, () => { + it('should mark the post as deleted and remove the rest of the thread', () => { + const state = deepFreeze({ + post1: {id: 'post1', file_ids: ['file'], has_reactions: true}, + comment1: {id: 'comment1', root_id: 'post1'}, + comment2: {id: 'comment2', root_id: 'post1'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.POST_DELETED, + data: {id: 'post1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).not.toBe(state.post1); + expect(nextState).toEqual({ + post1: {id: 'post1', file_ids: [], has_reactions: false, state: Posts.POST_DELETED}, + }); + }); + + it('should not remove the rest of the thread when deleting a comment', () => { + const state = deepFreeze({ + post1: {id: 'post1'}, + comment1: {id: 'comment1', root_id: 'post1'}, + comment2: {id: 'comment2', root_id: 'post1'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.POST_DELETED, + data: {id: 'comment1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState.comment1).not.toBe(state.comment1); + expect(nextState.comment2).toBe(state.comment2); + expect(nextState).toEqual({ + post1: {id: 'post1'}, + comment1: {id: 'comment1', root_id: 'post1', file_ids: [], has_reactions: false, state: Posts.POST_DELETED}, + comment2: {id: 'comment2', root_id: 'post1'}, + }); + }); + + it('should do nothing if the post is not loaded', () => { + const state = deepFreeze({ + post1: {id: 'post1', file_ids: ['file'], has_reactions: true}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.POST_DELETED, + data: {id: 'post2'}, + }); + + expect(nextState).toBe(state); + expect(nextState.post1).toBe(state.post1); + }); + }); + + describe(`removing a post (${PostTypes.POST_REMOVED})`, () => { + it('should remove the post and the rest and the rest of the thread', () => { + const state = deepFreeze({ + post1: {id: 'post1', file_ids: ['file'], has_reactions: true}, + comment1: {id: 'comment1', root_id: 'post1'}, + comment2: {id: 'comment2', root_id: 'post1'}, + post2: {id: 'post2'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'post1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post2).toBe(state.post2); + expect(nextState).toEqual({ + post2: {id: 'post2'}, + }); + }); + + it('should not remove the rest of the thread when removing a comment', () => { + const state = deepFreeze({ + post1: {id: 'post1'}, + comment1: {id: 'comment1', root_id: 'post1'}, + comment2: {id: 'comment2', root_id: 'post1'}, + post2: {id: 'post2'}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'comment1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState.comment1).not.toBe(state.comment1); + expect(nextState.comment2).toBe(state.comment2); + expect(nextState).toEqual({ + post1: {id: 'post1'}, + comment2: {id: 'comment2', root_id: 'post1'}, + post2: {id: 'post2'}, + }); + }); + + it('should do nothing if the post is not loaded', () => { + const state = deepFreeze({ + post1: {id: 'post1', file_ids: ['file'], has_reactions: true}, + }); + + const nextState = reducers.handlePosts(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'post2'}, + }); + + expect(nextState).toBe(state); + expect(nextState.post1).toBe(state.post1); + }); + }); + + for (const actionType of [ + ChannelTypes.RECEIVED_CHANNEL_DELETED, + ChannelTypes.DELETE_CHANNEL_SUCCESS, + ChannelTypes.LEAVE_CHANNEL, + ]) { + describe(`when a channel is deleted (${actionType})`, () => { + it('should remove any posts in that channel', () => { + const state = deepFreeze({ + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel2'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: false, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.post3).toBe(state.post3); + expect(nextState).toEqual({ + post3: {id: 'post3', channel_id: 'channel2'}, + }); + }); + + it('should do nothing if no posts in that channel are loaded', () => { + const state = deepFreeze({ + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel2'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: { + id: 'channel3', + viewArchivedChannels: false, + }, + }); + + expect(nextState).toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState.post2).toBe(state.post2); + expect(nextState.post3).toBe(state.post3); + }); + + it('should not remove any posts with viewArchivedChannels enabled', () => { + const state = deepFreeze({ + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel2'}, + }); + + const nextState = reducers.handlePosts(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: true, + }, + }); + + expect(nextState).toBe(state); + expect(nextState.post1).toBe(state.post1); + expect(nextState.post2).toBe(state.post2); + expect(nextState.post3).toBe(state.post3); + }); + }); + } +}); + +describe('pendingPostIds', () => { + describe('making a new pending post', () => { + it('should add new entries for pending posts', () => { + const state = deepFreeze(['1234']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: { + pending_post_id: 'abcd', + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual(['1234', 'abcd']); + }); + + it('should not add duplicate entries', () => { + const state = deepFreeze(['1234']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: { + pending_post_id: '1234', + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual(['1234']); + }); + + it('should do nothing for regular posts', () => { + const state = deepFreeze(['1234']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: { + id: 'abcd', + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual(['1234']); + }); + }); + + describe('removing a pending post', () => { + it('should remove an entry when its post is deleted', () => { + const state = deepFreeze(['1234', 'abcd']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.POST_REMOVED, + data: { + id: 'abcd', + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual(['1234']); + }); + + it('should do nothing without an entry for the post', () => { + const state = deepFreeze(['1234', 'abcd']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.POST_REMOVED, + data: { + id: 'wxyz', + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual(['1234', 'abcd']); + }); + }); + + describe('marking a pending post as completed', () => { + it('should remove an entry when its post is successfully created', () => { + const state = deepFreeze(['1234', 'abcd']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.RECEIVED_POST, + data: { + id: 'post', + pending_post_id: 'abcd', + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual(['1234']); + }); + + it('should do nothing without an entry for the post', () => { + const state = deepFreeze(['1234', 'abcd']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.RECEIVED_POST, + data: { + id: 'post', + pending_post_id: 'wxyz', + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual(['1234', 'abcd']); + }); + + it('should do nothing when receiving a non-pending post', () => { + const state = deepFreeze(['1234', 'abcd']); + + const nextState = reducers.handlePendingPosts(state, { + type: PostTypes.RECEIVED_POST, + data: { + id: 'post', + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual(['1234', 'abcd']); + }); + }); +}); + +describe('postsInChannel', () => { + describe('receiving a new post', () => { + it('should do nothing without posts loaded for the channel', () => { + const state = deepFreeze({}); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post1', channel_id: 'channel1'}, + }, {}, {}); + + expect(nextState).toBe(state); + expect(nextState).toEqual({}); + }); + + it('should store the new post when the channel is empty', () => { + const state = deepFreeze({ + channel1: [], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post1', channel_id: 'channel1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1'], recent: true}, + ], + }); + }); + + it('should store the new post when the channel has recent posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post1', channel_id: 'channel1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should not store the new post when the channel only has older posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: false}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post1', channel_id: 'channel1'}, + }); + + expect(nextState).toEqual({ + channel1: [ + {order: ['post2', 'post3'], recent: false}, {order: ['post1'], recent: true}, + ], + }); + }); + + it('should do nothing for a duplicate post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post1', channel_id: 'channel1'}, + }); + + expect(nextState).toBe(state); + }); + + it('should remove a previously pending post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['pending', 'post2', 'post1'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post3', channel_id: 'channel1', pending_post_id: 'pending'}, + }, {}, {post1: {create_at: 1}, post2: {create_at: 2}, post3: {create_at: 3}}); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post3', 'post2', 'post1'], recent: true}, + ], + }); + }); + + it('should just add the new post if the pending post was already removed', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_NEW_POST, + data: {id: 'post3', channel_id: 'channel1', pending_post_id: 'pending'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post3', 'post1', 'post2'], recent: true}, + ], + }); + }); + + it('should not include a previously removed post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'post1', channel_id: 'channel1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [{ + order: [], + recent: true, + }], + }); + }); + }); + + describe('receiving a single post', () => { + it('should replace a previously pending post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'pending', 'post2'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POST, + data: {id: 'post3', channel_id: 'channel1', pending_post_id: 'pending'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post3', 'post2'], recent: true}, + ], + }); + }); + + it('should do nothing for a pending post that was already removed', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POST, + data: {id: 'post3', channel_id: 'channel1', pending_post_id: 'pending'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + }); + + it('should do nothing for a post that was not previously pending', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'pending', 'post2'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POST, + data: {id: 'post3', channel_id: 'channel1'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'pending', 'post2'], recent: true}, + ], + }); + }); + + it('should do nothing for a post without posts loaded for the channel', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POST, + data: {id: 'post3', channel_id: 'channel2', pending_post_id: 'pending'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + }); + }); + + describe('receiving consecutive recent posts in the channel', () => { + it('should save posts in the correct order', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post4'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post3: nextPosts.post3, + }, + order: ['post1', 'post3'], + }, + recent: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should not save duplicate posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post4: nextPosts.post4, + }, + order: ['post2', 'post4'], + }, + recent: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should do nothing when receiving no posts for loaded channel', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + recent: true, + }, null, {}); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should make entry for channel with no posts', () => { + const state = deepFreeze({}); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + recent: true, + }, null, {}); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [{ + order: [], + recent: true, + }], + }); + }); + + it('should not save posts that are not in data.order', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + post3: nextPosts.post3, + post4: nextPosts.post4, + }, + order: ['post1', 'post2'], + }, + recent: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should not save posts in an older block, even if they may be adjacent', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + recent: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + {order: ['post1', 'post2'], recent: true}, + ], + }); + }); + + it('should not save posts in the recent block even if new posts may be adjacent', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + recent: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + {order: ['post1', 'post2'], recent: true}, + ], + }); + }); + + it('should add posts to non-recent block if there is overlap', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + recent: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + }); + + describe('receiving consecutive posts in the channel that are not recent', () => { + it('should save posts in the correct order', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post4'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post3: nextPosts.post3, + }, + order: ['post1', 'post3'], + }, + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should not save duplicate posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post4: nextPosts.post4, + }, + order: ['post2', 'post4'], + }, + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should do nothing when receiving no posts for loaded channel', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + recent: false, + }, null, {}); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should make entry for channel with no posts', () => { + const state = deepFreeze({}); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + recent: false, + }, null, {}); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [{ + order: [], + recent: false, + }], + }); + }); + + it('should not save posts that are not in data.order', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + post3: nextPosts.post3, + post4: nextPosts.post4, + }, + order: ['post1', 'post2'], + }, + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should not save posts in another block without overlap', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + {order: ['post1', 'post2'], recent: false}, + ], + }); + }); + + it('should add posts to recent block if there is overlap', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post3: nextPosts.post3, + }, + order: ['post2', 'post3'], + }, + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should save with chunk as oldest', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_IN_CHANNEL, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post3: nextPosts.post3, + }, + order: ['post2', 'post3'], + }, + recent: false, + oldest: true, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true, oldest: true}, + ], + }); + }); + }); + + describe('receiving posts since', () => { + it('should save posts in the channel in the correct order', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should not save older posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post4: nextPosts.post4, + }, + order: ['post1', 'post4'], + }, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should save any posts in between', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post4'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + post5: {id: 'post5', channel_id: 'channel1', create_at: 500}, + post6: {id: 'post6', channel_id: 'channel1', create_at: 300}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + post3: nextPosts.post5, + post4: nextPosts.post4, + post5: nextPosts.post5, + post6: nextPosts.post6, + }, + order: ['post1', 'post2', 'post3', 'post4', 'post5', 'post6'], + }, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should do nothing if only receiving updated posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post4: nextPosts.post4, + }, + order: ['post1', 'post4'], + }, + }, null, nextPosts); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should not save duplicate posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should do nothing when receiving no posts for loaded channel', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + }, null, {}); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should do nothing for channel with no posts', () => { + const state = deepFreeze({}); + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + page: 0, + }, null, {}); + + expect(nextState).toBe(state); + expect(nextState).toEqual({}); + }); + + it('should not save posts that are not in data.order', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post2', 'post3'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post4: nextPosts.post4, + }, + order: ['post1'], + }, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: true}, + ], + }); + }); + + it('should not save posts in an older block', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + }, null, nextPosts); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + ], + }); + }); + + it('should always save posts in the recent block', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_SINCE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + }); + + describe('receiving posts after', () => { + it('should save posts when channel is not loaded', () => { + const state = deepFreeze({}); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_AFTER, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + afterPostId: 'post3', + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should save posts when channel is empty', () => { + const state = deepFreeze({ + channel1: [], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_AFTER, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + afterPostId: 'post3', + recent: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should add posts to existing block', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_AFTER, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post2: nextPosts.post2, + }, + order: ['post1', 'post2'], + }, + afterPostId: 'post3', + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should merge adjacent posts if we have newer posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post4'], recent: false}, + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_AFTER, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post3: nextPosts.post3, + }, + order: ['post2', 'post3'], + }, + afterPostId: 'post4', + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should do nothing when no posts are received', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_AFTER, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + afterPostId: 'post1', + }, null, nextPosts); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + }); + }); + + describe('receiving posts before', () => { + it('should save posts when channel is not loaded', () => { + const state = deepFreeze({}); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post3: nextPosts.post3, + }, + order: ['post2', 'post3'], + }, + beforePostId: 'post1', + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should have oldest set to false', () => { + const state = deepFreeze({}); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post3: nextPosts.post3, + }, + order: ['post2', 'post3'], + }, + beforePostId: 'post1', + oldest: false, + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false, oldest: false}, + ], + }); + }); + + it('should save posts when channel is empty', () => { + const state = deepFreeze({ + channel1: [], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId: 'channel1', + data: { + posts: { + post2: nextPosts.post2, + post3: nextPosts.post3, + }, + order: ['post2', 'post3'], + }, + beforePostId: 'post1', + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should add posts to existing block', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: false}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId: 'channel1', + data: { + posts: { + post3: nextPosts.post3, + post4: nextPosts.post4, + }, + order: ['post3', 'post4'], + }, + beforePostId: 'post2', + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should merge adjacent posts if we have newer posts', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post4'], recent: false}, + {order: ['post1'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + post3: {id: 'post3', channel_id: 'channel1', create_at: 2000}, + post4: {id: 'post4', channel_id: 'channel1', create_at: 1000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId: 'channel1', + data: { + posts: { + post1: nextPosts.post1, + post3: nextPosts.post3, + }, + order: ['post2', 'post3', 'post4'], + }, + beforePostId: 'post1', + }, null, nextPosts); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: true}, + ], + }); + }); + + it('should do nothing when no posts are received', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + + const nextPosts = { + post1: {id: 'post1', channel_id: 'channel1', create_at: 4000}, + post2: {id: 'post2', channel_id: 'channel1', create_at: 3000}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.RECEIVED_POSTS_BEFORE, + channelId: 'channel1', + data: { + posts: {}, + order: [], + }, + beforePostId: 'post2', + }, null, nextPosts); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }); + }); + }); + + describe('deleting a post', () => { + it('should do nothing when deleting a post without comments', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post2, + }, prevPosts, null); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should remove comments on the post when deleting a post with comments', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1', root_id: 'post3'}, + post3: {id: 'post3', channel_id: 'channel1'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post3, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should remove comments from multiple blocks', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: false}, + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1', root_id: 'post4'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post4, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post2'], recent: false}, + {order: ['post4'], recent: false}, + ], + }); + }); + + it('should do nothing to blocks without comments', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: false}, + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1', root_id: 'post4'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post4, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState[0]).toBe(state[0]); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2'], recent: false}, + {order: ['post4'], recent: false}, + ], + }); + }); + + it('should do nothing when deleting a comment', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1', root_id: 'post3'}, + post3: {id: 'post3', channel_id: 'channel1'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post2, + }, prevPosts, null); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should do nothing if the post has not been loaded', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post4, + }, prevPosts, null); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should do nothing if no posts in the channel have been loaded', () => { + const state = deepFreeze({}); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post1, + }, prevPosts, null); + + expect(nextState).toBe(state); + expect(nextState).toEqual({}); + }); + + it('should remove empty blocks', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: false}, + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1', root_id: 'post4'}, + post3: {id: 'post3', channel_id: 'channel1', root_id: 'post4'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_DELETED, + data: prevPosts.post4, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post4'], recent: false}, + ], + }); + }); + }); + + describe('removing a post', () => { + it('should remove the post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: prevPosts.post2, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post3'], recent: false}, + ], + }); + }); + + it('should remove comments on the post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1', root_id: 'post3'}, + post3: {id: 'post3', channel_id: 'channel1'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: prevPosts.post3, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post4'], recent: false}, + ], + }); + }); + + it('should remove a comment without removing the root post', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1', root_id: 'post3'}, + post3: {id: 'post3', channel_id: 'channel1'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: prevPosts.post2, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post3', 'post4'], recent: false}, + ], + }); + }); + + it('should do nothing if the post has not been loaded', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: prevPosts.post4, + }, prevPosts, null); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + }); + }); + + it('should do nothing if no posts in the channel have been loaded', () => { + const state = deepFreeze({}); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: prevPosts.post1, + }, prevPosts, null); + + expect(nextState).toBe(state); + expect(nextState).toEqual({}); + }); + + it('should remove empty blocks', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2'], recent: false}, + {order: ['post3', 'post4'], recent: false}, + ], + }); + + const prevPosts = { + post1: {id: 'post1', channel_id: 'channel1', root_id: 'post4'}, + post2: {id: 'post2', channel_id: 'channel1'}, + post3: {id: 'post3', channel_id: 'channel1', root_id: 'post4'}, + post4: {id: 'post4', channel_id: 'channel1'}, + }; + + const nextState = reducers.postsInChannel(state, { + type: PostTypes.POST_REMOVED, + data: prevPosts.post4, + }, prevPosts, null); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + channel1: [ + {order: ['post2'], recent: false}, + ], + }); + }); + }); + + for (const actionType of [ + ChannelTypes.RECEIVED_CHANNEL_DELETED, + ChannelTypes.DELETE_CHANNEL_SUCCESS, + ChannelTypes.LEAVE_CHANNEL, + ]) { + describe(`when a channel is deleted (${actionType})`, () => { + it('should remove any posts in that channel', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + {order: ['post6', 'post7', 'post8'], recent: false}, + ], + channel2: [ + {order: ['post4', 'post5'], recent: false}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: false, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState.channel2).toBe(state.channel2); + expect(nextState).toEqual({ + channel2: [ + {order: ['post4', 'post5'], recent: false}, + ], + }); + }); + + it('should do nothing if no posts in that channel are loaded', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + channel2: [ + {order: ['post4', 'post5'], recent: false}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: actionType, + data: { + id: 'channel3', + viewArchivedChannels: false, + }, + }); + + expect(nextState).toBe(state); + expect(nextState.channel1).toBe(state.channel1); + expect(nextState.channel2).toBe(state.channel2); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + ], + channel2: [ + {order: ['post4', 'post5'], recent: false}, + ], + }); + }); + + it('should not remove any posts with viewArchivedChannels enabled', () => { + const state = deepFreeze({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + {order: ['post6', 'post7', 'post8'], recent: false}, + ], + channel2: [ + {order: ['post4', 'post5'], recent: false}, + ], + }); + + const nextState = reducers.postsInChannel(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: true, + }, + }); + + expect(nextState).toBe(state); + expect(nextState.channel1).toBe(state.channel1); + expect(nextState.channel2).toBe(state.channel2); + expect(nextState).toEqual({ + channel1: [ + {order: ['post1', 'post2', 'post3'], recent: false}, + {order: ['post6', 'post7', 'post8'], recent: false}, + ], + channel2: [ + {order: ['post4', 'post5'], recent: false}, + ], + }); + }); + }); + } +}); + +describe('mergePostBlocks', () => { + it('should do nothing with no blocks', () => { + const blocks = []; + const posts = {}; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).toBe(blocks); + }); + + it('should do nothing with only one block', () => { + const blocks = [ + {order: ['a'], recent: false}, + ]; + const posts = { + a: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).toBe(blocks); + }); + + it('should do nothing with two separate blocks', () => { + const blocks = [ + {order: ['a'], recent: false}, + {order: ['b'], recent: false}, + ]; + const posts = { + a: {create_at: 1000}, + b: {create_at: 1001}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).toBe(blocks); + }); + + it('should merge two blocks containing exactly the same posts', () => { + const blocks = [ + {order: ['a'], recent: false}, + {order: ['a'], recent: false}, + ]; + const posts = { + a: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks).toEqual([ + {order: ['a'], recent: false}, + ]); + }); + + it('should merge two blocks containing overlapping posts', () => { + const blocks = [ + {order: ['a', 'b', 'c'], recent: false}, + {order: ['b', 'c', 'd'], recent: false}, + ]; + const posts = { + a: {create_at: 1003}, + b: {create_at: 1002}, + c: {create_at: 1001}, + d: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks).toEqual([ + {order: ['a', 'b', 'c', 'd'], recent: false}, + ]); + }); + + it('should merge more than two blocks containing overlapping posts', () => { + const blocks = [ + {order: ['d', 'e'], recent: false}, + {order: ['a', 'b'], recent: false}, + {order: ['c', 'd'], recent: false}, + {order: ['b', 'c'], recent: false}, + ]; + const posts = { + a: {create_at: 1004}, + b: {create_at: 1003}, + c: {create_at: 1002}, + d: {create_at: 1001}, + e: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks).toEqual([ + {order: ['a', 'b', 'c', 'd', 'e'], recent: false}, + ]); + }); + + it('should not affect blocks that are not merged', () => { + const blocks = [ + {order: ['a', 'b'], recent: false}, + {order: ['b', 'c'], recent: false}, + {order: ['d', 'e'], recent: false}, + ]; + const posts = { + a: {create_at: 1004}, + b: {create_at: 1003}, + c: {create_at: 1002}, + d: {create_at: 1001}, + e: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks[1]).toBe(blocks[2]); + expect(nextBlocks).toEqual([ + {order: ['a', 'b', 'c'], recent: false}, + {order: ['d', 'e'], recent: false}, + ]); + }); + + it('should keep merged blocks marked as recent', () => { + const blocks = [ + {order: ['a', 'b'], recent: true}, + {order: ['b', 'c'], recent: false}, + ]; + const posts = { + a: {create_at: 1002}, + b: {create_at: 1001}, + c: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks).toEqual([ + {order: ['a', 'b', 'c'], recent: true}, + ]); + }); + + it('should keep merged blocks marked as oldest', () => { + const blocks = [ + {order: ['a', 'b'], oldest: true}, + {order: ['b', 'c'], oldest: false}, + ]; + const posts = { + a: {create_at: 1002}, + b: {create_at: 1001}, + c: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks).toEqual([ + {order: ['a', 'b', 'c'], oldest: true}, + ]); + }); + + it('should remove empty blocks', () => { + const blocks = [ + {order: ['a', 'b'], recent: true}, + {order: [], recent: false}, + ]; + const posts = { + a: {create_at: 1002}, + b: {create_at: 1001}, + c: {create_at: 1000}, + }; + + const nextBlocks = reducers.mergePostBlocks(blocks, posts); + + expect(nextBlocks).not.toBe(blocks); + expect(nextBlocks[0]).toBe(blocks[0]); + expect(nextBlocks).toEqual([ + {order: ['a', 'b'], recent: true}, + ]); + }); +}); + +describe('mergePostOrder', () => { + const tests = [ + { + name: 'empty arrays', + left: [], + right: [], + expected: [], + }, + { + name: 'empty left array', + left: [], + right: ['c', 'd'], + expected: ['c', 'd'], + }, + { + name: 'empty right array', + left: ['a', 'b'], + right: [], + expected: ['a', 'b'], + }, + { + name: 'distinct arrays', + left: ['a', 'b'], + right: ['c', 'd'], + expected: ['a', 'b', 'c', 'd'], + }, + { + name: 'overlapping arrays', + left: ['a', 'b', 'c', 'd'], + right: ['c', 'd', 'e', 'f'], + expected: ['a', 'b', 'c', 'd', 'e', 'f'], + }, + { + name: 'left array is start of right array', + left: ['a', 'b'], + right: ['a', 'b', 'c', 'd'], + expected: ['a', 'b', 'c', 'd'], + }, + { + name: 'right array is end of left array', + left: ['a', 'b', 'c', 'd'], + right: ['c', 'd'], + expected: ['a', 'b', 'c', 'd'], + }, + { + name: 'left array contains right array', + left: ['a', 'b', 'c', 'd'], + right: ['b', 'c'], + expected: ['a', 'b', 'c', 'd'], + }, + { + name: 'items in second array missing from first', + left: ['a', 'c'], + right: ['b', 'd', 'e', 'f'], + expected: ['a', 'b', 'c', 'd', 'e', 'f'], + }, + ]; + + const posts = { + a: {create_at: 10000}, + b: {create_at: 9000}, + c: {create_at: 8000}, + d: {create_at: 7000}, + e: {create_at: 6000}, + f: {create_at: 5000}, + }; + + for (const test of tests) { + it(test.name, () => { + const left = [...test.left]; + const right = [...test.right]; + + const actual = reducers.mergePostOrder(left, right, posts); + + expect(actual).toEqual(test.expected); + + // Arguments shouldn't be mutated + expect(left).toEqual(test.left); + expect(right).toEqual(test.right); + }); + } +}); + +describe('postsInThread', () => { + for (const actionType of [ + PostTypes.RECEIVED_POST, + PostTypes.RECEIVED_NEW_POST, + ]) { + describe(`receiving a single post (${actionType})`, () => { + it('should replace a previously pending comment', () => { + const state = deepFreeze({ + root1: ['comment1', 'pending', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: {id: 'comment3', root_id: 'root1', pending_post_id: 'pending'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2', 'comment3'], + }); + }); + + it('should do nothing for a pending comment that was already removed', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: {id: 'comment2', root_id: 'root1', pending_post_id: 'pending'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + + it('should store a comment that was not previously pending', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: {id: 'comment3', root_id: 'root1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2', 'comment3'], + }); + }); + + it('should store a comment without other comments loaded for the thread', () => { + const state = deepFreeze({}); + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: {id: 'comment1', root_id: 'root1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1'], + }); + }); + + it('should do nothing for a non-comment post', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: {id: 'root2'}, + }); + + expect(nextState).toBe(state); + expect(nextState.root1).toBe(state.root1); + expect(nextState).toEqual({ + root1: ['comment1'], + }); + }); + + it('should do nothing for a duplicate post', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: {id: 'comment1'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + }); + } + + for (const actionType of [ + PostTypes.RECEIVED_POSTS_AFTER, + PostTypes.RECEIVED_POSTS_BEFORE, + PostTypes.RECEIVED_POSTS_IN_CHANNEL, + PostTypes.RECEIVED_POSTS_SINCE, + ]) { + describe(`receiving posts in the channel (${actionType})`, () => { + it('should save comments without in the correct threads without sorting', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + comment2: {id: 'comment2', root_id: 'root1'}, + comment3: {id: 'comment3', root_id: 'root2'}, + comment4: {id: 'comment4', root_id: 'root1'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + order: [], + posts, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2', 'comment4'], + root2: ['comment3'], + }); + }); + + it('should not save not-comment posts', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + comment2: {id: 'comment2', root_id: 'root1'}, + root2: {id: 'root2'}, + comment3: {id: 'comment3', root_id: 'root2'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + order: [], + posts, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + }); + }); + + it('should not save duplicate posts', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + comment1: {id: 'comment2', root_id: 'root1'}, + comment2: {id: 'comment2', root_id: 'root1'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + order: [], + posts, + }, + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + + it('should do nothing when receiving no posts', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = {}; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + order: [], + posts, + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1'], + }); + }); + + it('should do nothing when receiving no comments', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + root2: {id: 'root2'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + order: [], + posts, + }, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1'], + }); + }); + }); + } + + describe('receiving posts in a thread', () => { + it('should save comments without sorting', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + comment2: {id: 'comment2', root_id: 'root1'}, + comment3: {id: 'comment3', root_id: 'root1'}, + }; + + const nextState = reducers.postsInThread(state, { + type: PostTypes.RECEIVED_POSTS_IN_THREAD, + data: { + order: [], + posts, + }, + rootId: 'root1', + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2', 'comment3'], + }); + }); + + it('should not save the root post', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + root2: {id: 'root2'}, + comment2: {id: 'comment2', root_id: 'root2'}, + comment3: {id: 'comment3', root_id: 'root2'}, + }; + + const nextState = reducers.postsInThread(state, { + type: PostTypes.RECEIVED_POSTS_IN_THREAD, + data: { + order: [], + posts, + }, + rootId: 'root2', + }); + + expect(nextState).not.toBe(state); + expect(nextState.root1).toBe(state.root1); + expect(nextState).toEqual({ + root1: ['comment1'], + root2: ['comment2', 'comment3'], + }); + }); + + it('should not save duplicate posts', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = { + comment1: {id: 'comment1', root_id: 'root1'}, + comment2: {id: 'comment2', root_id: 'root1'}, + }; + + const nextState = reducers.postsInThread(state, { + type: PostTypes.RECEIVED_POSTS_IN_THREAD, + data: { + order: [], + posts, + }, + rootId: 'root1', + }); + + expect(nextState).not.toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + + it('should do nothing when receiving no posts', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const posts = {}; + + const nextState = reducers.postsInThread(state, { + type: PostTypes.RECEIVED_POSTS_IN_THREAD, + data: { + order: [], + posts, + }, + rootId: 'root2', + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1'], + }); + }); + }); + + describe('deleting a post', () => { + it('should remove the thread when deleting the root post', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + }); + + const nextState = reducers.postsInThread(state, { + type: PostTypes.POST_DELETED, + data: {id: 'root1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.root2).toBe(state.root2); + expect(nextState).toEqual({ + root2: ['comment3'], + }); + }); + + it('should do nothing when deleting a comment', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: PostTypes.POST_DELETED, + data: {id: 'comment1'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + + it('should do nothing if deleting a post without comments', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: PostTypes.POST_DELETED, + data: {id: 'root2'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + }); + + describe('removing a post', () => { + it('should remove the thread when removing the root post', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + }); + + const nextState = reducers.postsInThread(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'root1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.root2).toBe(state.root2); + expect(nextState).toEqual({ + root2: ['comment3'], + }); + }); + + it('should remove an entry from the thread when removing a comment', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + }); + + const nextState = reducers.postsInThread(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'comment1', root_id: 'root1'}, + }); + + expect(nextState).not.toBe(state); + expect(nextState.root2).toBe(state.root2); + expect(nextState).toEqual({ + root1: ['comment2'], + root2: ['comment3'], + }); + }); + + it('should do nothing if removing a thread that has not been loaded', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const nextState = reducers.postsInThread(state, { + type: PostTypes.POST_REMOVED, + data: {id: 'root2'}, + }); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + }); + + for (const actionType of [ + ChannelTypes.RECEIVED_CHANNEL_DELETED, + ChannelTypes.DELETE_CHANNEL_SUCCESS, + ChannelTypes.LEAVE_CHANNEL, + ]) { + describe(`when a channel is deleted (${actionType})`, () => { + it('should remove any threads in that channel', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + root3: ['comment4'], + }); + + const prevPosts = { + root1: {id: 'root1', channel_id: 'channel1'}, + comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'}, + comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'}, + root2: {id: 'root2', channel_id: 'channel2'}, + comment3: {id: 'comment3', channel_id: 'channel2', root_id: 'root2'}, + root3: {id: 'root3', channel_id: 'channel1'}, + comment4: {id: 'comment3', channel_id: 'channel1', root_id: 'root3'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: false, + }, + }, prevPosts); + + expect(nextState).not.toBe(state); + expect(nextState.root2).toBe(state.root2); + expect(nextState).toEqual({ + root2: ['comment3'], + }); + }); + + it('should do nothing if no threads in that channel are loaded', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + }); + + const prevPosts = { + root1: {id: 'root1', channel_id: 'channel1'}, + comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'}, + comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + id: 'channel2', + viewArchivedChannels: false, + }, + }, prevPosts); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + }); + }); + + it('should not remove any posts with viewArchivedChannels enabled', () => { + const state = deepFreeze({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + }); + + const prevPosts = { + root1: {id: 'root1', channel_id: 'channel1'}, + comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'}, + comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'}, + root2: {id: 'root2', channel_id: 'channel2'}, + comment3: {id: 'comment3', channel_id: 'channel2', root_id: 'root2'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: true, + }, + }, prevPosts); + + expect(nextState).toBe(state); + expect(nextState).toEqual({ + root1: ['comment1', 'comment2'], + root2: ['comment3'], + }); + }); + + it('should not error if a post is missing from prevPosts', () => { + const state = deepFreeze({ + root1: ['comment1'], + }); + + const prevPosts = { + comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'}, + }; + + const nextState = reducers.postsInThread(state, { + type: actionType, + data: { + id: 'channel1', + viewArchivedChannels: false, + }, + }, prevPosts); + + expect(nextState).toBe(state); + }); + }); + } +}); + +describe('removeUnneededMetadata', () => { + it('without metadata', () => { + const post = deepFreeze({ + id: 'post', + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.equal(nextPost, post); + }); + + it('with empty metadata', () => { + const post = deepFreeze({ + id: 'post', + metadata: {}, + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.equal(nextPost, post); + }); + + it('should remove emojis', () => { + const post = deepFreeze({ + id: 'post', + metadata: { + emojis: [{name: 'emoji'}], + }, + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.notEqual(nextPost, post); + assert.deepEqual(nextPost, { + id: 'post', + metadata: {}, + }); + }); + + it('should remove files', () => { + const post = deepFreeze({ + id: 'post', + metadata: { + files: [{id: 'file', post_id: 'post'}], + }, + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.notEqual(nextPost, post); + assert.deepEqual(nextPost, { + id: 'post', + metadata: {}, + }); + }); + + it('should remove reactions', () => { + const post = deepFreeze({ + id: 'post', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '+1'}, + {user_id: 'efgh', emoji_name: '+1'}, + {user_id: 'abcd', emoji_name: '-1'}, + ], + }, + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.notEqual(nextPost, post); + assert.deepEqual(nextPost, { + id: 'post', + metadata: {}, + }); + }); + + it('should remove OpenGraph data', () => { + const post = deepFreeze({ + id: 'post', + metadata: { + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + url: 'https://example.com', + images: [{ + url: 'https://example.com/logo.png', + width: 100, + height: 100, + }], + }, + }], + }, + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.notEqual(nextPost, post); + assert.deepEqual(nextPost, { + id: 'post', + metadata: { + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + }], + }, + }); + }); + + it('should not affect non-OpenGraph embeds', () => { + const post = deepFreeze({ + id: 'post', + metadata: { + embeds: [ + {type: 'image', url: 'https://example.com/image'}, + {type: 'message_attachment'}, + ], + }, + props: { + attachments: [ + {text: 'This is an attachment'}, + ], + }, + }); + + const nextPost = reducers.removeUnneededMetadata(post); + + assert.equal(nextPost, post); + }); +}); + +describe('reactions', () => { + for (const actionType of [ + PostTypes.RECEIVED_NEW_POST, + PostTypes.RECEIVED_POST, + ]) { + describe(`single post received (${actionType})`, () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.equal(nextState, state); + }); + + it('no reactions in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: {reactions: []}, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: {}, + }); + }); + + it('should not clobber reactions when metadata empty', () => { + const state = deepFreeze({post: {name: 'smiley', post_id: 'post'}}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: {}, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.deepEqual(nextState, { + post: {name: 'smiley', post_id: 'post'}, + }); + }); + + it('should save reactions', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '+1'}, + {user_id: 'efgh', emoji_name: '+1'}, + {user_id: 'abcd', emoji_name: '-1'}, + ], + }, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: { + 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'}, + 'efgh-+1': {user_id: 'efgh', emoji_name: '+1'}, + 'abcd--1': {user_id: 'abcd', emoji_name: '-1'}, + }, + }); + }); + + it('should not save reaction for a deleted post', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + delete_at: '1571366424287', + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.equal(nextState, state); + }); + }); + } + + describe('receiving multiple posts', () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + }, + }, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.equal(nextState, state); + }); + + it('no reactions in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: {reactions: []}, + }, + }, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: {}, + }); + }); + + it('should save reactions', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '+1'}, + {user_id: 'efgh', emoji_name: '+1'}, + {user_id: 'abcd', emoji_name: '-1'}, + ], + }, + }, + }, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: { + 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'}, + 'efgh-+1': {user_id: 'efgh', emoji_name: '+1'}, + 'abcd--1': {user_id: 'abcd', emoji_name: '-1'}, + }, + }); + }); + + it('should save reactions for multiple posts', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '+1'}, + ], + }, + }, + post2: { + id: 'post2', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '-1'}, + ], + }, + }, + }, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post1: { + 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'}, + }, + post2: { + 'abcd--1': {user_id: 'abcd', emoji_name: '-1'}, + }, + }); + }); + + it('should save reactions for multiple posts except deleted posts', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '+1'}, + ], + }, + }, + post2: { + id: 'post2', + delete_at: '1571366424287', + metadata: { + reactions: [ + {user_id: 'abcd', emoji_name: '-1'}, + ], + }, + }, + }, + }, + }; + + const nextState = reducers.reactions(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post1: { + 'abcd-+1': {user_id: 'abcd', emoji_name: '+1'}, + }, + }); + }); + }); +}); + +describe('opengraph', () => { + for (const actionType of [ + PostTypes.RECEIVED_NEW_POST, + PostTypes.RECEIVED_POST, + ]) { + describe(`single post received (${actionType})`, () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.equal(nextState, state); + }); + + it('no embeds in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: {}, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.equal(nextState, state); + }); + + it('other types of embeds in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + embeds: [{ + type: 'image', + url: 'https://example.com/image.png', + }, { + type: 'message_attachment', + }], + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.equal(nextState, state); + }); + + it('should save opengraph data', () => { + const state = deepFreeze({}); + const action = { + type: actionType, + data: { + id: 'post', + metadata: { + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example', + description: 'Example description', + }, + }], + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post: {'https://example.com': action.data.metadata.embeds[0].data}, + }); + }); + }); + } + + describe('receiving multiple posts', () => { + it('no post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + }, + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.equal(nextState, state); + }); + + it('no embeds in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: {}, + }, + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.equal(nextState, state); + }); + + it('other types of embeds in post metadata', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post: { + id: 'post', + metadata: { + embeds: [{ + type: 'image', + url: 'https://example.com/image.png', + }, { + type: 'message_attachment', + }], + }, + }, + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.equal(nextState, state); + }); + + it('should save opengraph data', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example', + description: 'Example description', + }, + }], + }, + }, + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post1: {'https://example.com': action.data.posts.post1.metadata.embeds[0].data}, + }); + }); + + it('should save reactions for multiple posts', () => { + const state = deepFreeze({}); + const action = { + type: PostTypes.RECEIVED_POSTS, + data: { + posts: { + post1: { + id: 'post1', + metadata: { + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example', + description: 'Example description', + }, + }], + }, + }, + post2: { + id: 'post2', + metadata: { + embeds: [{ + type: 'opengraph', + url: 'https://google.ca', + data: { + title: 'Google', + description: 'Something about search', + }, + }], + }, + }, + }, + }, + }; + + const nextState = reducers.openGraph(state, action); + + assert.notEqual(nextState, state); + assert.deepEqual(nextState, { + post1: {'https://example.com': action.data.posts.post1.metadata.embeds[0].data}, + post2: {'https://google.ca': action.data.posts.post2.metadata.embeds[0].data}, + }); + }); + }); +}); + +describe('expandedURLs', () => { + it('should store the URLs on REDIRECT_LOCATION_SUCCESS', () => { + const state = deepFreeze({}); + const action = { + type: GeneralTypes.REDIRECT_LOCATION_SUCCESS, + data: { + url: 'a', + location: 'b', + }, + }; + + const nextState = reducers.expandedURLs(state, action); + assert.notEqual(state, nextState); + assert.deepEqual(nextState, { + a: 'b', + }); + }); + + it('should store the non-expanded URL on REDIRECT_LOCATION_FAILURE', () => { + const state = deepFreeze({}); + const action = { + type: GeneralTypes.REDIRECT_LOCATION_FAILURE, + data: { + url: 'b', + }, + }; + + const nextState = reducers.expandedURLs(state, action); + assert.notEqual(state, nextState); + assert.deepEqual(nextState, { + b: 'b', + }); + }); +}); + +describe('removeNonRecentEmptyPostBlocks', () => { + it('should filter empty blocks', () => { + const blocks = [{ + order: [], + recent: false, + }, { + order: ['1', '2'], + recent: false, + }]; + + const filteredBlocks = reducers.removeNonRecentEmptyPostBlocks(blocks); + assert.deepEqual(filteredBlocks, [{ + order: ['1', '2'], + recent: false, + }]); + }); + + it('should not filter empty recent block', () => { + const blocks = [{ + order: [], + recent: true, + }, { + order: ['1', '2'], + recent: false, + }, { + order: [], + recent: false, + }]; + + const filteredBlocks = reducers.removeNonRecentEmptyPostBlocks(blocks); + assert.deepEqual(filteredBlocks, [{ + order: [], + recent: true, + }, { + order: ['1', '2'], + recent: false, + }]); + }); +}); diff --git a/app/mm-redux/reducers/entities/posts.ts b/app/mm-redux/reducers/entities/posts.ts new file mode 100644 index 000000000..6fdea6e95 --- /dev/null +++ b/app/mm-redux/reducers/entities/posts.ts @@ -0,0 +1,1251 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {ChannelTypes, GeneralTypes, PostTypes, UserTypes} from '@mm-redux/action_types'; +import {Posts} from '../../constants'; +import {comparePosts} from '@mm-redux/utils/post_utils'; +import {Post, PostsState, PostOrderBlock, MessageHistory} from '@mm-redux/types/posts'; +import {RelationOneToOne, Dictionary, IDMappedObjects, RelationOneToMany} from '@mm-redux/types/utilities'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Reaction} from '@mm-redux/types/reactions'; + +export function removeUnneededMetadata(post: Post) { + if (!post.metadata) { + return post; + } + + const metadata = {...post.metadata}; + let changed = false; + + // These fields are stored separately + if (metadata.emojis) { + Reflect.deleteProperty(metadata, 'emojis'); + changed = true; + } + + if (metadata.files) { + Reflect.deleteProperty(metadata, 'files'); + changed = true; + } + + if (metadata.reactions) { + Reflect.deleteProperty(metadata, 'reactions'); + changed = true; + } + + if (metadata.embeds) { + let embedsChanged = false; + + const newEmbeds = metadata.embeds.map((embed) => { + if (embed.type !== 'opengraph') { + return embed; + } + + const newEmbed = {...embed}; + Reflect.deleteProperty(newEmbed, 'data'); + + embedsChanged = true; + + return newEmbed; + }); + + if (embedsChanged) { + metadata.embeds = newEmbeds; + changed = true; + } + } + + if (!changed) { + // Nothing changed + return post; + } + + return { + ...post, + metadata, + }; +} + +export function handlePosts(state: RelationOneToOne = {}, action: GenericAction) { + switch (action.type) { + case PostTypes.RECEIVED_POST: + case PostTypes.RECEIVED_NEW_POST: { + return handlePostReceived({...state}, action.data); + } + + case PostTypes.RECEIVED_POSTS: { + const posts = Object.values(action.data.posts) as Post[]; + + if (posts.length === 0) { + return state; + } + + const nextState = {...state}; + + for (const post of posts) { + handlePostReceived(nextState, post); + } + + return nextState; + } + + case PostTypes.POST_DELETED: { + const post: Post = action.data; + + if (!state[post.id]) { + return state; + } + + // Mark the post as deleted + const nextState = { + ...state, + [post.id]: { + ...state[post.id], + state: Posts.POST_DELETED, + file_ids: [], + has_reactions: false, + }, + }; + + // Remove any of its comments + for (const otherPost of Object.values(state)) { + if (otherPost.root_id === post.id) { + Reflect.deleteProperty(nextState, otherPost.id); + } + } + + return nextState; + } + + case PostTypes.POST_REMOVED: { + const post = action.data; + + if (!state[post.id]) { + return state; + } + + // Remove the post itself + const nextState = {...state}; + Reflect.deleteProperty(nextState, post.id); + + // Remove any of its comments + for (const otherPost of Object.values(state)) { + if (otherPost.root_id === post.id) { + Reflect.deleteProperty(nextState, otherPost.id); + } + } + + return nextState; + } + + case ChannelTypes.RECEIVED_CHANNEL_DELETED: + case ChannelTypes.DELETE_CHANNEL_SUCCESS: + case ChannelTypes.LEAVE_CHANNEL: { + if (action.data && action.data.viewArchivedChannels) { + // Nothing to do since we still want to store posts in archived channels + return state; + } + + const channelId = action.data.id; + + let postDeleted = false; + + // Remove any posts in the deleted channel + const nextState = {...state}; + for (const post of Object.values(state)) { + if (post.channel_id === channelId) { + Reflect.deleteProperty(nextState, post.id); + postDeleted = true; + } + } + + if (!postDeleted) { + // Nothing changed + return state; + } + + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function handlePostReceived(nextState: any, post: Post) { + if (nextState[post.id] && nextState[post.id].update_at >= post.update_at) { + // The stored post is newer than the one we've received + return nextState; + } + + if (post.delete_at > 0) { + // We've received a deleted post, so mark the post as deleted if we already have it + if (nextState[post.id]) { + nextState[post.id] = { + ...removeUnneededMetadata(post), + state: Posts.POST_DELETED, + file_ids: [], + has_reactions: false, + }; + } + } else { + nextState[post.id] = removeUnneededMetadata(post); + } + + // Delete any pending post that existed for this post + if (post.pending_post_id && post.id !== post.pending_post_id && nextState[post.pending_post_id]) { + Reflect.deleteProperty(nextState, post.pending_post_id); + } + + return nextState; +} + +export function handlePendingPosts(state: Array = [], action: GenericAction) { + switch (action.type) { + case PostTypes.RECEIVED_NEW_POST: { + const post = action.data; + + if (!post.pending_post_id) { + // This is not a pending post + return state; + } + + const index = state.indexOf(post.pending_post_id); + + if (index !== -1) { + // An entry already exists for this post + return state; + } + + // Add the new pending post ID + const nextState = [...state]; + nextState.push(post.pending_post_id); + + return nextState; + } + case PostTypes.POST_REMOVED: { + const post = action.data; + + const index = state.indexOf(post.id); + + if (index === -1) { + // There's nothing to remove + return state; + } + + // The post has been removed, so remove the entry for it + const nextState = [...state]; + nextState.splice(index, 1); + + return nextState; + } + case PostTypes.RECEIVED_POST: { + const post = action.data; + + if (!post.pending_post_id) { + // This isn't a pending post + return state; + } + + const index = state.indexOf(post.pending_post_id); + + if (index === -1) { + // There's nothing to remove + return state; + } + + // The post has actually been created, so remove the entry for it + const nextState = [...state]; + nextState.splice(index, 1); + + return nextState; + } + + default: + return state; + } +} + +export function postsInChannel(state: Dictionary> = {}, action: GenericAction, prevPosts: IDMappedObjects, nextPosts: Dictionary) { + switch (action.type) { + case PostTypes.RECEIVED_NEW_POST: { + const post = action.data as Post; + + const postsForChannel = state[post.channel_id]; + if (!postsForChannel) { + // Don't save newly created posts until the channel has been loaded + return state; + } + + const recentBlockIndex = postsForChannel.findIndex((block: PostOrderBlock) => block.recent); + + let nextRecentBlock: PostOrderBlock; + if (recentBlockIndex === -1) { + nextRecentBlock = { + order: [], + recent: true, + }; + } else { + const recentBlock = postsForChannel[recentBlockIndex]; + nextRecentBlock = { + ...recentBlock, + order: [...recentBlock.order], + }; + } + + let changed = false; + + // Add the new post to the channel + if (!nextRecentBlock.order.includes(post.id)) { + nextRecentBlock.order.unshift(post.id); + changed = true; + } + + // If this is a newly created post, remove any pending post that exists for it + if (post.pending_post_id && post.id !== post.pending_post_id) { + const index = nextRecentBlock.order.indexOf(post.pending_post_id); + + if (index !== -1) { + nextRecentBlock.order.splice(index, 1); + + // Need to re-sort to make sure any other pending posts come first + nextRecentBlock.order.sort((a, b) => { + return comparePosts(nextPosts[a], nextPosts[b]); + }); + changed = true; + } + } + + if (!changed) { + return state; + } + + const nextPostsForChannel = [...postsForChannel]; + + if (recentBlockIndex === -1) { + nextPostsForChannel.push(nextRecentBlock); + } else { + nextPostsForChannel[recentBlockIndex] = nextRecentBlock; + } + + return { + ...state, + [post.channel_id]: nextPostsForChannel, + }; + } + + case PostTypes.RECEIVED_POST: { + const post = action.data; + + // Receiving a single post doesn't usually affect the order of posts in a channel, except for when we've + // received a newly created post that was previously stored as pending + + if (!post.pending_post_id) { + return state; + } + + const postsForChannel = state[post.channel_id] || []; + + const recentBlockIndex = postsForChannel.findIndex((block: PostOrderBlock) => block.recent); + if (recentBlockIndex === -1) { + // Nothing to do since there's no recent block and only the recent block should contain pending posts + return state; + } + + const recentBlock = postsForChannel[recentBlockIndex]; + + // Replace the pending post with the newly created one + const index = recentBlock.order.indexOf(post.pending_post_id); + if (index === -1) { + // No pending post found to remove + return state; + } + + const nextRecentBlock = { + ...recentBlock, + order: [...recentBlock.order], + }; + + nextRecentBlock.order[index] = post.id; + + const nextPostsForChannel = [...postsForChannel]; + nextPostsForChannel[recentBlockIndex] = nextRecentBlock; + + return { + ...state, + [post.channel_id]: nextPostsForChannel, + }; + } + + case PostTypes.RECEIVED_POSTS_IN_CHANNEL: { + const {recent, oldest} = action; + const order = action.data.order; + + if (order.length === 0 && state[action.channelId]) { + // No new posts received when we already have posts + return state; + } + + const postsForChannel = state[action.channelId] || []; + let nextPostsForChannel = [...postsForChannel]; + + if (recent) { + // The newly received block is now the most recent, so unmark the current most recent block + const recentBlockIndex = postsForChannel.findIndex((block: PostOrderBlock) => block.recent); + if (recentBlockIndex !== -1) { + const recentBlock = postsForChannel[recentBlockIndex]; + + if (recentBlock.order.length === order.length && + recentBlock.order[0] === order[0] && + recentBlock.order[recentBlock.order.length - 1] === order[order.length - 1]) { + // The newly received posts are identical to the most recent block, so there's nothing to do + return state; + } + + // Unmark the most recent block since the new posts are more recent + const nextRecentBlock = { + ...recentBlock, + recent: false, + }; + + nextPostsForChannel[recentBlockIndex] = nextRecentBlock; + } + } + + // Add the new most recent block + nextPostsForChannel.push({ + order, + recent, + oldest, + }); + + // Merge overlapping blocks + nextPostsForChannel = mergePostBlocks(nextPostsForChannel, nextPosts); + + return { + ...state, + [action.channelId]: nextPostsForChannel, + }; + } + + case PostTypes.RECEIVED_POSTS_AFTER: { + const order = action.data.order; + const afterPostId = action.afterPostId; + + if (order.length === 0) { + // No posts received + return state; + } + + const postsForChannel = state[action.channelId] || []; + + // Add a new block including the previous post and then have mergePostBlocks sort out any overlap or duplicates + const newBlock = { + order: [...order, afterPostId], + recent: action.recent, + }; + + let nextPostsForChannel = [...postsForChannel, newBlock]; + nextPostsForChannel = mergePostBlocks(nextPostsForChannel, nextPosts); + + return { + ...state, + [action.channelId]: nextPostsForChannel, + }; + } + + case PostTypes.RECEIVED_POSTS_BEFORE: { + const {order} = action.data; + const {beforePostId, oldest} = action; + + if (order.length === 0) { + // No posts received + return state; + } + + const postsForChannel = state[action.channelId] || []; + + // Add a new block including the next post and then have mergePostBlocks sort out any overlap or duplicates + const newBlock = { + order: [beforePostId, ...order], + recent: false, + oldest, + }; + + let nextPostsForChannel = [...postsForChannel, newBlock]; + nextPostsForChannel = mergePostBlocks(nextPostsForChannel, nextPosts); + + return { + ...state, + [action.channelId]: nextPostsForChannel, + }; + } + + case PostTypes.RECEIVED_POSTS_SINCE: { + const order = action.data.order; + + if (order.length === 0 && state[action.channelId]) { + // No new posts received when we already have posts + return state; + } + + const postsForChannel = state[action.channelId] || []; + + const recentBlockIndex = postsForChannel.findIndex((block: PostOrderBlock) => block.recent); + if (recentBlockIndex === -1) { + // Nothing to do since this shouldn't be dispatched if we haven't loaded the most recent posts yet + return state; + } + + const recentBlock = postsForChannel[recentBlockIndex]; + + const mostOldestCreateAt = nextPosts[recentBlock.order[recentBlock.order.length - 1]].create_at; + + const nextRecentBlock: PostOrderBlock = { + ...recentBlock, + order: [...recentBlock.order], + }; + + // Add any new posts to the most recent block while skipping ones that were only updated + for (let i = order.length - 1; i >= 0; i--) { + const postId = order[i]; + + if (!nextPosts[postId]) { + // the post was removed from the list + continue; + } + + if (nextPosts[postId].create_at <= mostOldestCreateAt) { + // This is an old post + continue; + } + + if (nextRecentBlock.order.indexOf(postId) !== -1) { + // This postId exists so no need to add it again + continue; + } + + // This post is newer than what we have + nextRecentBlock.order.unshift(postId); + } + + if (nextRecentBlock.order.length === recentBlock.order.length) { + // Nothing was added + return state; + } + + nextRecentBlock.order.sort((a, b) => { + return comparePosts(nextPosts[a], nextPosts[b]); + }); + + const nextPostsForChannel = [...postsForChannel]; + nextPostsForChannel[recentBlockIndex] = nextRecentBlock; + + return { + ...state, + [action.channelId]: nextPostsForChannel, + }; + } + + case PostTypes.POST_DELETED: { + const post = action.data; + + // Deleting a post removes its comments from the order, but does not remove the post itself + + const postsForChannel = state[post.channel_id] || []; + if (postsForChannel.length === 0) { + return state; + } + + let changed = false; + + let nextPostsForChannel = [...postsForChannel]; + for (let i = 0; i < nextPostsForChannel.length; i++) { + const block = nextPostsForChannel[i]; + + // Remove any comments for this post + const nextOrder = block.order.filter((postId: string) => prevPosts[postId].root_id !== post.id); + + if (nextOrder.length !== block.order.length) { + nextPostsForChannel[i] = { + ...block, + order: nextOrder, + }; + + changed = true; + } + } + + if (!changed) { + // Nothing was removed + return state; + } + + nextPostsForChannel = removeNonRecentEmptyPostBlocks(nextPostsForChannel); + + return { + ...state, + [post.channel_id]: nextPostsForChannel, + }; + } + + case PostTypes.POST_REMOVED: { + const post = action.data; + + // Removing a post removes it as well as its comments + + const postsForChannel = state[post.channel_id] || []; + if (postsForChannel.length === 0) { + return state; + } + + let changed = false; + + // Remove the post and its comments from the channel + let nextPostsForChannel = [...postsForChannel]; + for (let i = 0; i < nextPostsForChannel.length; i++) { + const block = nextPostsForChannel[i]; + + const nextOrder = block.order.filter((postId: string) => postId !== post.id && prevPosts[postId].root_id !== post.id); + + if (nextOrder.length !== block.order.length) { + nextPostsForChannel[i] = { + ...block, + order: nextOrder, + }; + + changed = true; + } + } + + if (!changed) { + // Nothing was removed + return state; + } + + nextPostsForChannel = removeNonRecentEmptyPostBlocks(nextPostsForChannel); + + return { + ...state, + [post.channel_id]: nextPostsForChannel, + }; + } + + case ChannelTypes.RECEIVED_CHANNEL_DELETED: + case ChannelTypes.DELETE_CHANNEL_SUCCESS: + case ChannelTypes.LEAVE_CHANNEL: { + if (action.data && action.data.viewArchivedChannels) { + // Nothing to do since we still want to store posts in archived channels + return state; + } + + const channelId = action.data.id; + + if (!state[channelId]) { + // Nothing to do since we have no posts for this channel + return state; + } + + // Remove the entry for the deleted channel + const nextState = {...state}; + Reflect.deleteProperty(nextState, channelId); + + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +export function removeNonRecentEmptyPostBlocks(blocks: PostOrderBlock[]) { + return blocks.filter((block: PostOrderBlock) => block.order.length !== 0 || block.recent); +} + +export function mergePostBlocks(blocks: PostOrderBlock[], posts: Dictionary) { + let nextBlocks = [...blocks]; + + // Remove any blocks that may have become empty by removing posts + nextBlocks = removeNonRecentEmptyPostBlocks(blocks); + + // If a channel does not have any posts(Experimental feature where join and leave messages don't exist) + // return the previous state i.e an empty block + if (!nextBlocks.length) { + return blocks; + } + + // Sort blocks so that the most recent one comes first + nextBlocks.sort((a, b) => { + const aStartsAt = posts[a.order[0]].create_at; + const bStartsAt = posts[b.order[0]].create_at; + + return bStartsAt - aStartsAt; + }); + + // Merge adjacent blocks + let i = 0; + while (i < nextBlocks.length - 1) { + // Since we know the start of a is more recent than the start of b, they'll overlap if the last post in a is + // older than the first post in b + const a = nextBlocks[i]; + const aEndsAt = posts[a.order[a.order.length - 1]].create_at; + + const b = nextBlocks[i + 1]; + const bStartsAt = posts[b.order[0]].create_at; + + if (aEndsAt <= bStartsAt) { + // The blocks overlap, so combine them and remove the second block + nextBlocks[i] = { + order: mergePostOrder(a.order, b.order, posts), + }; + + nextBlocks[i].recent = a.recent || b.recent; + nextBlocks[i].oldest = a.oldest || b.oldest; + + nextBlocks.splice(i + 1, 1); + + // Do another iteration on this index since it may need to be merged into the next + } else { + // The blocks don't overlap, so move on to the next one + i += 1; + } + } + + if (blocks.length === nextBlocks.length) { + // No changes were made + return blocks; + } + + return nextBlocks; +} + +export function mergePostOrder(left: string[], right: string[], posts: Dictionary) { + const result = [...left]; + + // Add without duplicates + const seen = new Set(left); + for (const id of right) { + if (seen.has(id)) { + continue; + } + + result.push(id); + } + + if (result.length === left.length) { + // No new items added + return left; + } + + // Re-sort so that the most recent post comes first + result.sort((a, b) => posts[b].create_at - posts[a].create_at); + + return result; +} + +export function postsInThread(state: RelationOneToMany = {}, action: GenericAction, prevPosts: Dictionary) { + switch (action.type) { + case PostTypes.RECEIVED_NEW_POST: + case PostTypes.RECEIVED_POST: { + const post = action.data; + + if (!post.root_id) { + // Only store comments, not the root post + return state; + } + + const postsForThread = state[post.root_id] || []; + const nextPostsForThread = [...postsForThread]; + + let changed = false; + + if (!postsForThread.includes(post.id)) { + nextPostsForThread.push(post.id); + changed = true; + } + + // If this is a new non-pending post, remove any pending post that exists for it + if (post.pending_post_id && post.id !== post.pending_post_id) { + const index = nextPostsForThread.indexOf(post.pending_post_id); + + if (index !== -1) { + nextPostsForThread.splice(index, 1); + changed = true; + } + } + + if (!changed) { + return state; + } + + return { + ...state, + [post.root_id]: nextPostsForThread, + }; + } + + case PostTypes.RECEIVED_POSTS_AFTER: + case PostTypes.RECEIVED_POSTS_BEFORE: + case PostTypes.RECEIVED_POSTS_IN_CHANNEL: + case PostTypes.RECEIVED_POSTS_SINCE: { + const newPosts: Post[] = Object.values(action.data.posts); + + if (newPosts.length === 0) { + // Nothing to add + return state; + } + + const nextState: Dictionary = {}; + + for (const post of newPosts) { + if (!post.root_id) { + // Only store comments, not the root post + continue; + } + + const postsForThread = state[post.root_id] || []; + const nextPostsForThread = nextState[post.root_id] || [...postsForThread]; + + // Add the post to the thread + if (!nextPostsForThread.includes(post.id)) { + nextPostsForThread.push(post.id); + } + + nextState[post.root_id] = nextPostsForThread; + } + + if (Object.keys(nextState).length === 0) { + return state; + } + + return { + ...state, + ...nextState, + }; + } + + case PostTypes.RECEIVED_POSTS_IN_THREAD: { + const newPosts: Post[] = Object.values(action.data.posts); + + if (newPosts.length === 0) { + // Nothing to add + return state; + } + + const postsForThread = state[action.rootId] || []; + const nextPostsForThread = [...postsForThread]; + + for (const post of newPosts) { + if (post.root_id !== action.rootId) { + // Only store comments + continue; + } + + if (nextPostsForThread.includes(post.id)) { + // Don't store duplicates + continue; + } + + nextPostsForThread.push(post.id); + } + + return { + ...state, + [action.rootId]: nextPostsForThread, + }; + } + + case PostTypes.POST_DELETED: { + const post = action.data; + + const postsForThread = state[post.id]; + if (!postsForThread) { + // Nothing to remove + return state; + } + + const nextState = {...state}; + Reflect.deleteProperty(nextState, post.id); + + return nextState; + } + + case PostTypes.POST_REMOVED: { + const post = action.data; + + if (post.root_id) { + // This is a comment, so remove it from the thread + const postsForThread = state[post.root_id]; + if (!postsForThread) { + return state; + } + + const index = postsForThread.findIndex((postId) => postId === post.id); + if (index === -1) { + return state; + } + + const nextPostsForThread = [...postsForThread]; + nextPostsForThread.splice(index, 1); + + return { + ...state, + [post.root_id]: nextPostsForThread, + }; + } + + // This is not a comment, so remove any comments on it + const postsForThread = state[post.id]; + if (!postsForThread) { + return state; + } + + const nextState = {...state}; + Reflect.deleteProperty(nextState, post.id); + + return nextState; + } + + case ChannelTypes.RECEIVED_CHANNEL_DELETED: + case ChannelTypes.DELETE_CHANNEL_SUCCESS: + case ChannelTypes.LEAVE_CHANNEL: { + if (action.data && action.data.viewArchivedChannels) { + // Nothing to do since we still want to store posts in archived channels + return state; + } + + const channelId = action.data.id; + + let postDeleted = false; + + // Remove entries for any thread in the channel + const nextState = {...state}; + for (const rootId of Object.keys(state)) { + if (prevPosts[rootId] && prevPosts[rootId].channel_id === channelId) { + Reflect.deleteProperty(nextState, rootId); + postDeleted = true; + } + } + + if (!postDeleted) { + // Nothing was actually removed + return state; + } + + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function selectedPostId(state = '', action: GenericAction) { + switch (action.type) { + case PostTypes.RECEIVED_POST_SELECTED: + return action.data; + case UserTypes.LOGOUT_SUCCESS: + return ''; + default: + return state; + } +} + +function currentFocusedPostId(state = '', action: GenericAction) { + switch (action.type) { + case PostTypes.RECEIVED_FOCUSED_POST: + return action.data; + case UserTypes.LOGOUT_SUCCESS: + return ''; + default: + return state; + } +} + +export function reactions(state: RelationOneToOne> = {}, action: GenericAction) { + switch (action.type) { + case PostTypes.RECEIVED_REACTIONS: { + const reactionsList = action.data; + const nextReactions: Dictionary = {}; + reactionsList.forEach((reaction: Reaction) => { + nextReactions[reaction.user_id + '-' + reaction.emoji_name] = reaction; + }); + + return { + ...state, + [action.postId!]: nextReactions, + }; + } + case PostTypes.RECEIVED_REACTION: { + const reaction = action.data as Reaction; + const nextReactions = {...(state[reaction.post_id] || {})}; + nextReactions[reaction.user_id + '-' + reaction.emoji_name] = reaction; + + return { + ...state, + [reaction.post_id]: nextReactions, + }; + } + case PostTypes.REACTION_DELETED: { + const reaction = action.data; + const nextReactions = {...(state[reaction.post_id] || {})}; + if (!nextReactions[reaction.user_id + '-' + reaction.emoji_name]) { + return state; + } + + Reflect.deleteProperty(nextReactions, reaction.user_id + '-' + reaction.emoji_name); + + return { + ...state, + [reaction.post_id]: nextReactions, + }; + } + + case PostTypes.RECEIVED_NEW_POST: + case PostTypes.RECEIVED_POST: { + const post = action.data; + + return storeReactionsForPost(state, post); + } + + case PostTypes.RECEIVED_POSTS: { + const posts = Object.values(action.data.posts); + + return posts.reduce(storeReactionsForPost, state); + } + + case PostTypes.POST_DELETED: + case PostTypes.POST_REMOVED: { + const post = action.data; + + if (post && state[post.id]) { + const nextState = {...state}; + Reflect.deleteProperty(nextState, post.id); + + return nextState; + } + + return state; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function storeReactionsForPost(state: any, post: Post) { + if (!post.metadata || !post.metadata.reactions || post.delete_at > 0) { + return state; + } + + const reactionsForPost: Dictionary = {}; + if (post.metadata.reactions && post.metadata.reactions.length > 0) { + for (const reaction of post.metadata.reactions) { + reactionsForPost[reaction.user_id + '-' + reaction.emoji_name] = reaction; + } + } + + return { + ...state, + [post.id]: reactionsForPost, + }; +} + +export function openGraph(state: RelationOneToOne = {}, action: GenericAction) { + switch (action.type) { + case PostTypes.RECEIVED_OPEN_GRAPH_METADATA: { + const nextState = {...state}; + nextState[action.url] = action.data; + + return nextState; + } + + case PostTypes.RECEIVED_NEW_POST: + case PostTypes.RECEIVED_POST: { + const post = action.data; + + return storeOpenGraphForPost(state, post); + } + case PostTypes.RECEIVED_POSTS: { + const posts = Object.values(action.data.posts); + + return posts.reduce(storeOpenGraphForPost, state); + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function storeOpenGraphForPost(state: any, post: Post) { + if (!post.metadata || !post.metadata.embeds) { + return state; + } + + return post.metadata.embeds.reduce((nextState, embed) => { + if (embed.type !== 'opengraph' || !embed.data) { + // Not an OpenGraph embed + return nextState; + } + + const postIdState = nextState[post.id] ? {...nextState[post.id], [embed.url]: embed.data} : {[embed.url]: embed.data}; + return { + ...nextState, + [post.id]: postIdState, + }; + }, state); +} + +function messagesHistory(state: Partial = {}, action: GenericAction) { + switch (action.type) { + case PostTypes.ADD_MESSAGE_INTO_HISTORY: { + const nextIndex: Dictionary = {}; + let nextMessages = state.messages ? [...state.messages] : []; + nextMessages.push(action.data); + nextIndex[Posts.MESSAGE_TYPES.POST] = nextMessages.length; + nextIndex[Posts.MESSAGE_TYPES.COMMENT] = nextMessages.length; + + if (nextMessages.length > Posts.MAX_PREV_MSGS) { + nextMessages = nextMessages.slice(1, Posts.MAX_PREV_MSGS + 1); + } + + return { + messages: nextMessages, + index: nextIndex, + }; + } + case PostTypes.RESET_HISTORY_INDEX: { + const index: Dictionary = {}; + index[Posts.MESSAGE_TYPES.POST] = -1; + index[Posts.MESSAGE_TYPES.COMMENT] = -1; + + const messages = state.messages || []; + const nextIndex = state.index ? {...state.index} : index; + nextIndex[action.data] = messages.length; + return { + messages: state.messages, + index: nextIndex, + }; + } + case PostTypes.MOVE_HISTORY_INDEX_BACK: { + const index: Dictionary = {}; + index[Posts.MESSAGE_TYPES.POST] = -1; + index[Posts.MESSAGE_TYPES.COMMENT] = -1; + + const nextIndex = state.index ? {...state.index} : index; + if (nextIndex[action.data] > 0) { + nextIndex[action.data]--; + } + return { + messages: state.messages, + index: nextIndex, + }; + } + case PostTypes.MOVE_HISTORY_INDEX_FORWARD: { + const index: Dictionary = {}; + index[Posts.MESSAGE_TYPES.POST] = -1; + index[Posts.MESSAGE_TYPES.COMMENT] = -1; + + const messages = state.messages || []; + const nextIndex = state.index ? {...state.index} : index; + if (nextIndex[action.data] < messages.length) { + nextIndex[action.data]++; + } + return { + messages: state.messages, + index: nextIndex, + }; + } + case UserTypes.LOGOUT_SUCCESS: { + const index: Dictionary = {}; + index[Posts.MESSAGE_TYPES.POST] = -1; + index[Posts.MESSAGE_TYPES.COMMENT] = -1; + + return { + messages: [], + index, + }; + } + default: + return state; + } +} + +export function expandedURLs(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case GeneralTypes.REDIRECT_LOCATION_SUCCESS: + return { + ...state, + [action.data.url]: action.data.location, + }; + case GeneralTypes.REDIRECT_LOCATION_FAILURE: + return { + ...state, + [action.data.url]: action.data.url, + }; + default: + return state; + } +} + +export default function(state: Partial = {}, action: GenericAction) { + const nextPosts = handlePosts(state.posts, action); + const nextPostsInChannel = postsInChannel(state.postsInChannel, action, state.posts!, nextPosts); + + const nextState = { + + // Object mapping post ids to post objects + posts: nextPosts, + + // Array that contains the pending post ids for those messages that are in transition to being created + pendingPostIds: handlePendingPosts(state.pendingPostIds, action), + + // Object mapping channel ids to an array of posts ids in that channel with the most recent post first + postsInChannel: nextPostsInChannel, + + // Object mapping post root ids to an array of posts ids of comments (but not the root post) in that thread + // with no guaranteed order + postsInThread: postsInThread(state.postsInThread, action, state.posts!), + + // The current selected post + selectedPostId: selectedPostId(state.selectedPostId, action), + + // The current selected focused post (permalink view) + currentFocusedPostId: currentFocusedPostId(state.currentFocusedPostId, action), + + // Object mapping post ids to an object of emoji reactions using userId-emojiName as keys + reactions: reactions(state.reactions, action), + + // Object mapping URLs to their relevant opengraph metadata for link previews + openGraph: openGraph(state.openGraph, action), + + // History of posts and comments + messagesHistory: messagesHistory(state.messagesHistory, action), + + expandedURLs: expandedURLs(state.expandedURLs, action), + }; + + if (state.posts === nextState.posts && state.postsInChannel === nextState.postsInChannel && + state.postsInThread === nextState.postsInThread && + state.pendingPostIds === nextState.pendingPostIds && + state.selectedPostId === nextState.selectedPostId && + state.currentFocusedPostId === nextState.currentFocusedPostId && + state.reactions === nextState.reactions && + state.openGraph === nextState.openGraph && + state.messagesHistory === nextState.messagesHistory && + state.expandedURLs === nextState.expandedURLs) { + // None of the children have changed so don't even let the parent object change + return state; + } + + return nextState; +} diff --git a/app/mm-redux/reducers/entities/preferences.ts b/app/mm-redux/reducers/entities/preferences.ts new file mode 100644 index 000000000..3d8f57565 --- /dev/null +++ b/app/mm-redux/reducers/entities/preferences.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {PreferenceTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {Dictionary} from '@mm-redux/types/utilities'; + +function getKey(preference: PreferenceType) { + return `${preference.category}--${preference.name}`; +} + +function setAllPreferences(preferences: Array): any { + const nextState: any = {}; + + if (preferences) { + for (const preference of preferences) { + nextState[getKey(preference)] = preference; + } + } + + return nextState; +} + +function myPreferences(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case PreferenceTypes.RECEIVED_ALL_PREFERENCES: + return setAllPreferences(action.data); + + case UserTypes.LOGIN: // Used by the mobile app + return setAllPreferences(action.data.preferences); + + case PreferenceTypes.RECEIVED_PREFERENCES: { + const nextState = {...state}; + + if (action.data) { + for (const preference of action.data) { + nextState[getKey(preference)] = preference; + } + } + + return nextState; + } + case PreferenceTypes.DELETED_PREFERENCES: { + const nextState = {...state}; + + if (action.data) { + for (const preference of action.data) { + Reflect.deleteProperty(nextState, getKey(preference)); + } + } + + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +export default combineReducers({ + + // object where the key is the category-name and has the corresponding value + myPreferences, +}); diff --git a/app/mm-redux/reducers/entities/roles.ts b/app/mm-redux/reducers/entities/roles.ts new file mode 100644 index 000000000..3e5be7ff4 --- /dev/null +++ b/app/mm-redux/reducers/entities/roles.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {RoleTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Dictionary} from '@mm-redux/types/utilities'; +import {Role} from '@mm-redux/types/roles'; + +function pending(state: Set = new Set(), action: GenericAction) { + switch (action.type) { + case RoleTypes.SET_PENDING_ROLES: + return action.data; + case UserTypes.LOGOUT_SUCCESS: + return new Set(); + default: + return state; + } +} + +function roles(state: Dictionary = {}, action: GenericAction) { + switch (action.type) { + case RoleTypes.RECEIVED_ROLES: { + if (action.data) { + const nextState = {...state}; + for (const role of action.data) { + nextState[role.name] = role; + } + return nextState; + } + + return state; + } + case RoleTypes.ROLE_DELETED: { + if (action.data) { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.name); + return nextState; + } + + return state; + } + case RoleTypes.RECEIVED_ROLE: { + if (action.data) { + const nextState = {...state}; + nextState[action.data.name] = action.data; + return nextState; + } + + return state; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +export default combineReducers({ + + // object where the key is the category-name and has the corresponding value + roles, + pending, +}); diff --git a/app/mm-redux/reducers/entities/schemes.ts b/app/mm-redux/reducers/entities/schemes.ts new file mode 100644 index 000000000..ec7985cb1 --- /dev/null +++ b/app/mm-redux/reducers/entities/schemes.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {SchemeTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +import {SchemesState, Scheme} from '@mm-redux/types/schemes'; + +function schemes(state: { + [x: string]: Scheme; +} = {}, action: GenericAction): { + [x: string]: Scheme; + } { + switch (action.type) { + case SchemeTypes.CREATED_SCHEME: + case SchemeTypes.PATCHED_SCHEME: + case SchemeTypes.RECEIVED_SCHEME: { + return { + ...state, + [action.data.id]: action.data, + }; + } + + case SchemeTypes.RECEIVED_SCHEMES: { + const nextState = {...state}; + for (const scheme of action.data) { + nextState[scheme.id] = scheme; + } + return nextState; + } + + case SchemeTypes.DELETED_SCHEME: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data.schemeId); + return nextState; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +export default (combineReducers({ + schemes, +}) as (b: SchemesState, a: GenericAction) => SchemesState); diff --git a/app/mm-redux/reducers/entities/search.test.js b/app/mm-redux/reducers/entities/search.test.js new file mode 100644 index 000000000..07fa2eca1 --- /dev/null +++ b/app/mm-redux/reducers/entities/search.test.js @@ -0,0 +1,288 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import { + PostTypes, + SearchTypes, + UserTypes, +} from '@mm-redux/action_types'; +import reducer from '@mm-redux/reducers/entities/search'; + +describe('reducers.entities.search', () => { + describe('results', () => { + it('initial state', () => { + const inputState = undefined; // eslint-disable-line no-undef + const action = {}; + const expectedState = []; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + }); + + describe('SearchTypes.RECEIVED_SEARCH_POSTS', () => { + it('first results received', () => { + const inputState = []; + const action = { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: { + order: ['abcd', 'efgh'], + posts: { + abcd: {id: 'abcd'}, + efgh: {id: 'efgh'}, + }, + }, + }; + const expectedState = ['abcd', 'efgh']; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + }); + + it('multiple results received', () => { + const inputState = ['1234', '1235']; + const action = { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: { + order: ['abcd', 'efgh'], + posts: { + abcd: {id: 'abcd'}, + efgh: {id: 'efgh'}, + }, + }, + }; + const expectedState = ['abcd', 'efgh']; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + }); + }); + + describe('PostTypes.POST_REMOVED', () => { + it('post in results', () => { + const inputState = ['abcd', 'efgh']; + const action = { + type: PostTypes.POST_REMOVED, + data: { + id: 'efgh', + }, + }; + const expectedState = ['abcd']; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + }); + + it('post not in results', () => { + const inputState = ['abcd', 'efgh']; + const action = { + type: PostTypes.POST_REMOVED, + data: { + id: '1234', + }, + }; + const expectedState = ['abcd', 'efgh']; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + assert.equal(actualState.results, inputState); + }); + }); + + describe('SearchTypes.REMOVE_SEARCH_POSTS', () => { + const inputState = ['abcd', 'efgh']; + const action = { + type: SearchTypes.REMOVE_SEARCH_POSTS, + }; + const expectedState = []; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + }); + + describe('UserTypes.LOGOUT_SUCCESS', () => { + const inputState = ['abcd', 'efgh']; + const action = { + type: UserTypes.LOGOUT_SUCCESS, + }; + const expectedState = []; + + const actualState = reducer({results: inputState}, action); + assert.deepEqual(actualState.results, expectedState); + }); + }); + + describe('matches', () => { + it('initial state', () => { + const inputState = undefined; // eslint-disable-line no-undef + const action = {}; + const expectedState = {}; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + + describe('SearchTypes.RECEIVED_SEARCH_POSTS', () => { + it('no matches received', () => { + const inputState = {}; + const action = { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: { + order: ['abcd', 'efgh'], + posts: { + abcd: {id: 'abcd'}, + efgh: {id: 'efgh'}, + }, + }, + }; + const expectedState = {}; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + + it('first results received', () => { + const inputState = {}; + const action = { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: { + order: ['abcd', 'efgh'], + posts: { + abcd: {id: 'abcd'}, + efgh: {id: 'efgh'}, + }, + matches: { + abcd: ['test', 'testing'], + efgh: ['tests'], + }, + }, + }; + const expectedState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + + it('multiple results received', () => { + const inputState = { + 1234: ['foo', 'bar'], + 5678: ['foo'], + }; + const action = { + type: SearchTypes.RECEIVED_SEARCH_POSTS, + data: { + order: ['abcd', 'efgh'], + posts: { + abcd: {id: 'abcd'}, + efgh: {id: 'efgh'}, + }, + matches: { + abcd: ['test', 'testing'], + efgh: ['tests'], + }, + }, + }; + const expectedState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + }); + + describe('PostTypes.POST_REMOVED', () => { + it('post in results', () => { + const inputState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + const action = { + type: PostTypes.POST_REMOVED, + data: { + id: 'efgh', + }, + }; + const expectedState = { + abcd: ['test', 'testing'], + }; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + + it('post not in results', () => { + const inputState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + const action = { + type: PostTypes.POST_REMOVED, + data: { + id: '1234', + }, + }; + const expectedState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + assert.equal(actualState.matches, inputState); + }); + }); + + describe('SearchTypes.REMOVE_SEARCH_POSTS', () => { + const inputState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + const action = { + type: SearchTypes.REMOVE_SEARCH_POSTS, + }; + const expectedState = []; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + + describe('UserTypes.LOGOUT_SUCCESS', () => { + const inputState = { + abcd: ['test', 'testing'], + efgh: ['tests'], + }; + const action = { + type: UserTypes.LOGOUT_SUCCESS, + }; + const expectedState = []; + + const actualState = reducer({matches: inputState}, action); + assert.deepEqual(actualState.matches, expectedState); + }); + }); + + describe('pinned', () => { + it('do not show multiples of the same post', () => { + const inputState = { + abcd: ['1234', '5678'], + }; + const action = { + type: PostTypes.RECEIVED_POST, + data: { + id: '5678', + is_pinned: true, + channel_id: 'abcd', + }, + }; + + const actualState = reducer({pinned: inputState}, action); + assert.deepEqual(actualState.pinned, inputState); + }); + }); +}); diff --git a/app/mm-redux/reducers/entities/search.ts b/app/mm-redux/reducers/entities/search.ts new file mode 100644 index 000000000..a97e67691 --- /dev/null +++ b/app/mm-redux/reducers/entities/search.ts @@ -0,0 +1,313 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {PostTypes, PreferenceTypes, SearchTypes, UserTypes} from '@mm-redux/action_types'; +import {Preferences} from '../../constants'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {GenericAction} from '@mm-redux/types/actions'; +import {Post} from '@mm-redux/types/posts'; +import {Dictionary} from '@mm-redux/types/utilities'; +import {Search} from '@mm-redux/types/search'; + +function results(state: Array = [], action: GenericAction) { + switch (action.type) { + case SearchTypes.RECEIVED_SEARCH_POSTS: { + if (action.isGettingMore) { + return [...new Set(state.concat(action.data.order))]; + } + return action.data.order; + } + case PostTypes.POST_REMOVED: { + const postId = action.data ? action.data.id : null; + const index = state.indexOf(postId); + if (index !== -1) { + const newState = [...state]; + newState.splice(index, 1); + return newState; + } + return state; + } + case SearchTypes.REMOVE_SEARCH_POSTS: + case UserTypes.LOGOUT_SUCCESS: + return []; + + default: + return state; + } +} + +function matches(state: Dictionary> = {}, action: GenericAction) { + switch (action.type) { + case SearchTypes.RECEIVED_SEARCH_POSTS: + if (action.isGettingMore) { + return Object.assign({}, state, action.data.matches); + } + return action.data.matches || {}; + case PostTypes.POST_REMOVED: { + if (!state[action.data.id]) { + return state; + } + + const newState = {...state}; + Reflect.deleteProperty(newState, action.data.id); + return newState; + } + case SearchTypes.REMOVE_SEARCH_POSTS: + case UserTypes.LOGOUT_SUCCESS: + return []; + + default: + return state; + } +} + +function flagged(state: Array = [], action: GenericAction) { + switch (action.type) { + case SearchTypes.RECEIVED_SEARCH_FLAGGED_POSTS: { + return action.data.order; + } + case PostTypes.POST_REMOVED: { + const postId = action.data ? action.data.id : null; + const index = state.indexOf(postId); + if (index !== -1) { + const newState = [...state]; + newState.splice(index, 1); + return newState; + } + return state; + } + case PreferenceTypes.RECEIVED_PREFERENCES: { + if (action.data) { + const nextState = [...state]; + let hasNewFlaggedPosts = false; + action.data.forEach((pref: PreferenceType) => { + if (pref.category === Preferences.CATEGORY_FLAGGED_POST) { + const exists = nextState.find((p) => p === pref.name); + if (!exists) { + hasNewFlaggedPosts = true; + nextState.unshift(pref.name); + } + } + }); + + return hasNewFlaggedPosts ? nextState : state; + } + + return state; + } + case PreferenceTypes.DELETED_PREFERENCES: { + if (action.data) { + const nextState = [...state]; + let flaggedPostsRemoved = false; + action.data.forEach((pref: PreferenceType) => { + if (pref.category === Preferences.CATEGORY_FLAGGED_POST) { + const index = state.indexOf(pref.name); + if (index !== -1) { + flaggedPostsRemoved = true; + nextState.splice(index, 1); + } + } + }); + + return flaggedPostsRemoved ? nextState : state; + } + + return state; + } + case SearchTypes.REMOVE_SEARCH_POSTS: + case UserTypes.LOGOUT_SUCCESS: + return []; + + default: + return state; + } +} + +function removePinnedPost(state: Dictionary>, post: Post) { + if (post && state[post.channel_id]) { + const postId = post.id; + const channelId = post.channel_id; + const pinnedPosts = [...state[channelId]]; + const index = pinnedPosts.indexOf(postId); + + if (index !== -1) { + pinnedPosts.splice(index, 1); + return { + ...state, + [channelId]: pinnedPosts, + }; + } + } + + return state; +} + +function pinned(state: Dictionary> = {}, action: GenericAction) { + switch (action.type) { + case SearchTypes.RECEIVED_SEARCH_PINNED_POSTS: { + const {channelId, pinned: posts} = action.data; + return { + ...state, + [channelId]: posts.order.reverse(), + }; + } + case PostTypes.POST_DELETED: + case PostTypes.POST_REMOVED: { + return removePinnedPost(state, action.data); + } + case PostTypes.RECEIVED_POST: { + const post = action.data; + if (post && post.is_pinned) { + const channelId = post.channel_id; + let pinnedPosts: string[] = []; + + if (state[channelId]) { + pinnedPosts = [...state[channelId]]; + } + + if (!pinnedPosts.includes(post.id)) { + pinnedPosts.unshift(post.id); + } + + return { + ...state, + [channelId]: pinnedPosts, + }; + } + + return removePinnedPost(state, action.data); + } + case SearchTypes.REMOVE_SEARCH_PINNED_POSTS: { + const {channelId} = action.data; + const nextState = {...state}; + if (nextState[channelId]) { + Reflect.deleteProperty(nextState, channelId); + return nextState; + } + + return state; + } + case UserTypes.LOGOUT_SUCCESS: + return []; + + default: + return state; + } +} + +function recent(state: Dictionary> = {}, action: GenericAction) { + const {data, type} = action; + + switch (type) { + case SearchTypes.RECEIVED_SEARCH_TERM: { + const nextState = {...state}; + const {teamId, params} = data; + const {terms, isOrSearch} = params || {}; + const team = [...(nextState[teamId] || [])]; + const index = team.findIndex((r) => r.terms === terms); + if (index === -1) { + team.push({terms, isOrSearch}); + } else { + team[index] = {terms, isOrSearch}; + } + return { + ...nextState, + [teamId]: team, + }; + } + case SearchTypes.REMOVE_SEARCH_TERM: { + const nextState = {...state}; + const {teamId, terms} = data; + const team = [...(nextState[teamId] || [])]; + const index = team.findIndex((r) => r.terms === terms); + + if (index !== -1) { + team.splice(index, 1); + + return { + ...nextState, + [teamId]: team, + }; + } + + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function current(state: any = {}, action: GenericAction) { + const {data, type} = action; + switch (type) { + case SearchTypes.RECEIVED_SEARCH_TERM: { + const nextState = {...state}; + const {teamId, params, isEnd} = data; + return { + ...nextState, + [teamId]: { + params, + isEnd, + }, + }; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function isSearchingTerm(state = false, action: GenericAction) { + switch (action.type) { + case SearchTypes.SEARCH_POSTS_REQUEST: + return !action.isGettingMore; + case SearchTypes.SEARCH_POSTS_SUCCESS: + return false; + default: + return state; + } +} + +function isSearchGettingMore(state = false, action: GenericAction) { + switch (action.type) { + case SearchTypes.SEARCH_POSTS_REQUEST: + return action.isGettingMore; + case SearchTypes.SEARCH_POSTS_SUCCESS: + return false; + default: + return state; + } +} + +export default combineReducers({ + + // An ordered array with posts ids of flagged posts + flagged, + + // An Object where every key is a channel id mapping to an ordered array with posts ids of pinned posts + pinned, + + // An ordered array with posts ids from the search results + results, + + // Object where every key is a post id mapping to an array of matched words in that post + matches, + + // Object where every key is a team composed with + // an object where the key is the term and the value indicates is "or" search + recent, + + // Object holding the current searches for every team + current, + + // Boolean true if we are are searching initally + isSearchingTerm, + + // Boolean true if we are getting more search results + isSearchGettingMore, +}); diff --git a/app/mm-redux/reducers/entities/teams.test.js b/app/mm-redux/reducers/entities/teams.test.js new file mode 100644 index 000000000..4562dbc5d --- /dev/null +++ b/app/mm-redux/reducers/entities/teams.test.js @@ -0,0 +1,110 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {TeamTypes} from '@mm-redux/action_types'; +import teamsReducer from '@mm-redux/reducers/entities/teams'; + +describe('Reducers.teams.myMembers', () => { + it('initial state', async () => { + let state = {}; + + state = teamsReducer(state, {}); + assert.deepEqual(state.myMembers, {}, 'initial state'); + }); + + it('RECEIVED_MY_TEAM_MEMBER', async () => { + const myMember1 = {user_id: 'user_id_1', team_id: 'team_id_1', delete_at: 0, mention_count: 0, msg_count: 0}; + const myMember2 = {user_id: 'user_id_2', team_id: 'team_id_2', delete_at: 0, mention_count: 0, msg_count: 0}; + const myMember3 = {user_id: 'user_id_3', team_id: 'team_id_3', delete_at: 1, mention_count: 0, msg_count: 0}; + + let state = {myMembers: {team_id_1: myMember1}}; + const testAction = { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, + data: myMember2, + result: {team_id_1: myMember1, team_id_2: myMember2}, + }; + + state = teamsReducer(state, testAction); + assert.deepEqual(state.myMembers, testAction.result); + + testAction.data = myMember3; + state = teamsReducer(state, {}); + assert.deepEqual(state.myMembers, testAction.result); + }); + + it('RECEIVED_MY_TEAM_MEMBERS', async () => { + let state = {}; + const myMember1 = {user_id: 'user_id_1', team_id: 'team_id_1', delete_at: 0, mention_count: 0, msg_count: 0}; + const myMember2 = {user_id: 'user_id_2', team_id: 'team_id_2', delete_at: 0, mention_count: 0, msg_count: 0}; + const myMember3 = {user_id: 'user_id_3', team_id: 'team_id_3', delete_at: 1, mention_count: 0, msg_count: 0}; + const testAction = { + type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS, + data: [myMember1, myMember2, myMember3], + result: {team_id_1: myMember1, team_id_2: myMember2}, + }; + + state = teamsReducer(state, testAction); + assert.deepEqual(state.myMembers, testAction.result); + + state = teamsReducer(state, {}); + assert.deepEqual(state.myMembers, testAction.result); + }); + + it('RECEIVED_TEAMS_LIST', async () => { + const team1 = {name: 'team-1', id: 'team_id_1', delete_at: 0}; + const team2 = {name: 'team-2', id: 'team_id_2', delete_at: 0}; + const team3 = {name: 'team-3', id: 'team_id_3', delete_at: 0}; + + let state = { + myMembers: { + team_id_1: {...team1, msg_count: 0, mention_count: 0}, + team_id_2: {...team2, msg_count: 0, mention_count: 0}, + }, + }; + + const testAction = { + type: TeamTypes.RECEIVED_TEAMS_LIST, + data: [team3], + result: { + team_id_1: {...team1, msg_count: 0, mention_count: 0}, + team_id_2: {...team2, msg_count: 0, mention_count: 0}, + }, + }; + + // do not add a team when it's not on the teams.myMembers list + state = teamsReducer(state, testAction); + assert.deepEqual(state.myMembers, testAction.result); + + // remove deleted team to teams.myMembers list + team2.delete_at = 1; + testAction.data = [team2]; + state = teamsReducer(state, testAction); + assert.deepEqual(state.myMembers, {team_id_1: {...team1, msg_count: 0, mention_count: 0}}); + }); + + it('RECEIVED_TEAMS', async () => { + const myMember1 = {user_id: 'user_id_1', team_id: 'team_id_1', delete_at: 0, mention_count: 0, msg_count: 0}; + const myMember2 = {user_id: 'user_id_2', team_id: 'team_id_2', delete_at: 0, mention_count: 0, msg_count: 0}; + const myMember3 = {user_id: 'user_id_3', team_id: 'team_id_3', delete_at: 0, mention_count: 0, msg_count: 0}; + + let state = {myMembers: {team_id_1: myMember1, team_id_2: myMember2}}; + + const testAction = { + type: TeamTypes.RECEIVED_TEAMS, + data: {team_id_3: myMember3}, + result: {team_id_1: myMember1, team_id_2: myMember2}, + }; + + // do not add a team when it's not on the teams.myMembers list + state = teamsReducer(state, testAction); + assert.deepEqual(state.myMembers, testAction.result); + + // remove deleted team to teams.myMembers list + myMember2.delete_at = 1; + testAction.data = {team_id_2: myMember2}; + state = teamsReducer(state, testAction); + assert.deepEqual(state.myMembers, {team_id_1: myMember1}); + }); +}); \ No newline at end of file diff --git a/app/mm-redux/reducers/entities/teams.ts b/app/mm-redux/reducers/entities/teams.ts new file mode 100644 index 000000000..321468f3a --- /dev/null +++ b/app/mm-redux/reducers/entities/teams.ts @@ -0,0 +1,470 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {ChannelTypes, TeamTypes, UserTypes, SchemeTypes, GroupTypes} from '@mm-redux/action_types'; +import {teamListToMap} from '@mm-redux/utils/team_utils'; +import {Team, TeamMembership, TeamUnread} from '@mm-redux/types/teams'; +import {UserProfile} from '@mm-redux/types/users'; +import {RelationOneToOne, IDMappedObjects} from '@mm-redux/types/utilities'; +import {GenericAction} from '@mm-redux/types/actions'; + +function currentTeamId(state = '', action: GenericAction) { + switch (action.type) { + case TeamTypes.SELECT_TEAM: + return action.data; + + case UserTypes.LOGOUT_SUCCESS: + return ''; + default: + return state; + } +} + +function teams(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case TeamTypes.RECEIVED_TEAMS_LIST: + case SchemeTypes.RECEIVED_SCHEME_TEAMS: + return Object.assign({}, state, teamListToMap(action.data)); + case UserTypes.LOGIN: // Used by the mobile app + return Object.assign({}, state, teamListToMap(action.data.teams)); + case TeamTypes.RECEIVED_TEAMS: + return Object.assign({}, state, action.data); + + case TeamTypes.CREATED_TEAM: + case TeamTypes.UPDATED_TEAM: + case TeamTypes.PATCHED_TEAM: + case TeamTypes.RECEIVED_TEAM: + return { + ...state, + [action.data.id]: action.data, + }; + + case TeamTypes.RECEIVED_TEAM_DELETED: { + const nextState = {...state}; + const teamId = action.data.id; + if (nextState.hasOwnProperty(teamId)) { + Reflect.deleteProperty(nextState, teamId); + return nextState; + } + + return state; + } + + case TeamTypes.UPDATED_TEAM_SCHEME: { + const {teamId, schemeId} = action.data; + const team = state[teamId]; + + if (!team) { + return state; + } + + return {...state, [teamId]: {...team, scheme_id: schemeId}}; + } + + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function myMembers(state: RelationOneToOne = {}, action: GenericAction) { + function updateState(receivedTeams: IDMappedObjects = {}, currentState: RelationOneToOne = {}) { + return Object.keys(receivedTeams).forEach((teamId) => { + if (receivedTeams[teamId].delete_at > 0 && currentState[teamId]) { + Reflect.deleteProperty(currentState, teamId); + } + }); + } + + switch (action.type) { + case TeamTypes.RECEIVED_MY_TEAM_MEMBER: { + const nextState = {...state}; + const member = action.data; + if (member.delete_at === 0) { + nextState[member.team_id] = member; + } + return nextState; + } + case TeamTypes.RECEIVED_MY_TEAM_MEMBERS: { + const nextState: RelationOneToOne = {}; + const members = action.data; + for (const m of members) { + if (m.delete_at == null || m.delete_at === 0) { + const prevMember = state[m.team_id] || {mention_count: 0, msg_count: 0}; + nextState[m.team_id] = { + ...prevMember, + ...m, + }; + } + } + return nextState; + } + case TeamTypes.RECEIVED_TEAMS_LIST: { + const nextState = {...state}; + const receivedTeams = teamListToMap(action.data); + updateState(receivedTeams, nextState); + return nextState; + } + case TeamTypes.RECEIVED_TEAMS: { + const nextState = {...state}; + const receivedTeams = action.data; + updateState(receivedTeams, nextState); + return nextState; + } + case TeamTypes.RECEIVED_MY_TEAM_UNREADS: { + const nextState = {...state}; + const unreads = action.data; + for (const u of unreads) { + const msgCount = u.msg_count < 0 ? 0 : u.msg_count; + const mentionCount = u.mention_count < 0 ? 0 : u.mention_count; + const m = { + ...state[u.team_id], + mention_count: mentionCount, + msg_count: msgCount, + }; + nextState[u.team_id] = m; + } + + return nextState; + } + case ChannelTypes.INCREMENT_UNREAD_MSG_COUNT: { + const {teamId, amount, onlyMentions} = action.data; + const member = state[teamId]; + + if (!member) { + // Don't keep track of unread posts until we've loaded the actual team member + return state; + } + + if (onlyMentions) { + // Incrementing the msg_count marks the team as unread, so don't do that if these posts shouldn't be unread + return state; + } + + return { + ...state, + [teamId]: { + ...member, + msg_count: member.msg_count + amount, + }, + }; + } + case ChannelTypes.DECREMENT_UNREAD_MSG_COUNT: { + const {teamId, amount} = action.data; + const member = state[teamId]; + + if (!member) { + // Don't keep track of unread posts until we've loaded the actual team member + return state; + } + + return { + ...state, + [teamId]: { + ...member, + msg_count: Math.max(member.msg_count - Math.abs(amount), 0), + }, + }; + } + case ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT: { + const {teamId, amount} = action.data; + const member = state[teamId]; + + if (!member) { + // Don't keep track of unread posts until we've loaded the actual team member + return state; + } + + return { + ...state, + [teamId]: { + ...member, + mention_count: member.mention_count + amount, + }, + }; + } + case ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT: { + const {teamId, amount} = action.data; + const member = state[teamId]; + + if (!member) { + // Don't keep track of unread posts until we've loaded the actual team member + return state; + } + + return { + ...state, + [teamId]: { + ...member, + mention_count: Math.max(member.mention_count - amount, 0), + }, + }; + } + + case TeamTypes.LEAVE_TEAM: + case TeamTypes.RECEIVED_TEAM_DELETED: { + const nextState = {...state}; + const data = action.data; + Reflect.deleteProperty(nextState, data.id); + return nextState; + } + case TeamTypes.UPDATED_TEAM_MEMBER_SCHEME_ROLES: { + return updateMyTeamMemberSchemeRoles(state, action); + } + + case ChannelTypes.POST_UNREAD_SUCCESS: { + const {teamId, deltaMsgs, mentionCount, msgCount} = action.data; + + const teamState = state[teamId]; + if (!teamState) { + return state; + } + + const newTeamState = { + ...teamState, + msg_count: (typeof teamState.msg_count === 'undefined' ? msgCount : teamState.msg_count - deltaMsgs), + mention_count: (typeof teamState.mention_count === 'undefined' ? mentionCount : teamState.mention_count + mentionCount), + }; + + return {...state, [teamId]: newTeamState}; + } + + case UserTypes.LOGIN: {// Used by the mobile app + const {teamMembers, teamUnreads} = action.data; + const nextState = {...state}; + + for (const m of teamMembers) { + if (m.delete_at == null || m.delete_at === 0) { + const unread = teamUnreads.find((u: TeamUnread) => u.team_id === m.team_id); + if (unread) { + m.mention_count = unread.mention_count; + m.msg_count = unread.msg_count; + } + nextState[m.team_id] = m; + } + } + + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function membersInTeam(state: RelationOneToOne> = {}, action: GenericAction) { + switch (action.type) { + case TeamTypes.RECEIVED_MEMBER_IN_TEAM: { + const data = action.data; + const members = {...(state[data.team_id] || {})}; + members[data.user_id] = data; + return { + ...state, + [data.team_id]: members, + }; + } + case TeamTypes.RECEIVED_TEAM_MEMBERS: { + const data = action.data; + if (data && data.length) { + const nextState = {...state}; + for (const member of data) { + if (nextState[member.team_id]) { + nextState[member.team_id] = {...nextState[member.team_id]}; + } else { + nextState[member.team_id] = {}; + } + nextState[member.team_id][member.user_id] = member; + } + + return nextState; + } + + return state; + } + case TeamTypes.RECEIVED_MEMBERS_IN_TEAM: { + const data = action.data; + if (data && data.length) { + const teamId = data[0].team_id; + const members = {...(state[teamId] || {})}; + for (const member of data) { + members[member.user_id] = member; + } + + return { + ...state, + [teamId]: members, + }; + } + + return state; + } + case TeamTypes.REMOVE_MEMBER_FROM_TEAM: { + const data = action.data; + const members = state[data.team_id]; + if (members) { + const nextState = {...members}; + Reflect.deleteProperty(nextState, data.user_id); + return { + ...state, + [data.team_id]: nextState, + }; + } + + return state; + } + case TeamTypes.RECEIVED_TEAM_DELETED: { + const nextState = {...state}; + const teamId = action.data.id; + if (nextState.hasOwnProperty(teamId)) { + Reflect.deleteProperty(nextState, teamId); + return nextState; + } + + return state; + } + case TeamTypes.UPDATED_TEAM_MEMBER_SCHEME_ROLES: { + return updateTeamMemberSchemeRoles(state, action); + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function stats(state: any = {}, action: GenericAction) { + switch (action.type) { + case TeamTypes.RECEIVED_TEAM_STATS: { + const stat = action.data; + return { + ...state, + [stat.team_id]: stat, + }; + } + case TeamTypes.RECEIVED_TEAM_DELETED: { + const nextState = {...state}; + const teamId = action.data.id; + if (nextState.hasOwnProperty(teamId)) { + Reflect.deleteProperty(nextState, teamId); + return nextState; + } + + return state; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +function groupsAssociatedToTeam(state: RelationOneToOne = {}, action: GenericAction) { + switch (action.type) { + case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM: { + const {teamID, groups, totalGroupCount} = action.data; + const nextState = {...state}; + const associatedGroupIDs = new Set(state[teamID] ? state[teamID].ids : []); + for (const group of groups) { + associatedGroupIDs.add(group.id); + } + nextState[teamID] = {ids: Array.from(associatedGroupIDs), totalCount: totalGroupCount}; + return nextState; + } + case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM: { + const {teamID, groups} = action.data; + const nextState = {...state}; + const associatedGroupIDs = new Set([]); + for (const group of groups) { + associatedGroupIDs.add(group.id); + } + const ids = Array.from(associatedGroupIDs); + nextState[teamID] = {ids, totalCount: ids.length}; + return nextState; + } + case GroupTypes.RECEIVED_GROUPS_NOT_ASSOCIATED_TO_TEAM: { + const {teamID, groups} = action.data; + const nextState = {...state}; + const associatedGroupIDs = new Set(state[teamID] ? state[teamID].ids : []); + for (const group of groups) { + associatedGroupIDs.delete(group.id); + } + nextState[teamID] = {ids: Array.from(associatedGroupIDs), totalCount: associatedGroupIDs.size}; + return nextState; + } + default: + return state; + } +} + +function updateTeamMemberSchemeRoles(state: RelationOneToOne>, action: GenericAction) { + const {teamId, userId, isSchemeUser, isSchemeAdmin} = action.data; + const team = state[teamId]; + if (team) { + const member = team[userId]; + if (member) { + return { + ...state, + [teamId]: { + ...state[teamId], + [userId]: { + ...state[teamId][userId], + scheme_user: isSchemeUser, + scheme_admin: isSchemeAdmin, + }, + }, + }; + } + } + return state; +} + +function updateMyTeamMemberSchemeRoles(state: RelationOneToOne, action: GenericAction) { + const {teamId, userId, isSchemeUser, isSchemeAdmin} = action.data; + const member = state[teamId]; + if (member) { + return { + ...state, + [teamId]: { + ...state[teamId], + scheme_user: isSchemeUser, + scheme_admin: isSchemeAdmin, + }, + }; + } + return state; +} + +function totalCount(state = 0, action: GenericAction) { + switch (action.type) { + case TeamTypes.RECEIVED_TOTAL_TEAM_COUNT: { + return action.data; + } + default: + return state; + } +} + +export default combineReducers({ + + // the current selected team + currentTeamId, + + // object where every key is the team id and has and object with the team detail + teams, + + // object where every key is the team id and has and object with the team members detail + myMembers, + + // object where every key is the team id and has an object of members in the team where the key is user id + membersInTeam, + + // object where every key is the team id and has an object with the team stats + stats, + + groupsAssociatedToTeam, + + totalCount, +}); diff --git a/app/mm-redux/reducers/entities/typing.test.js b/app/mm-redux/reducers/entities/typing.test.js new file mode 100644 index 000000000..95ed767e2 --- /dev/null +++ b/app/mm-redux/reducers/entities/typing.test.js @@ -0,0 +1,292 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {WebsocketEvents} from '../../constants'; + +import typingReducer from '@mm-redux/reducers/entities/typing'; + +import TestHelper from 'test/test_helper'; + +describe('Reducers.Typing', () => { + it('initial state', async () => { + let state = {}; + + state = typingReducer( + state, + {}, + ); + assert.deepEqual( + state, + {}, + 'initial state', + ); + }); + + it('WebsocketEvents.TYPING', async () => { + let state = {}; + + const id1 = TestHelper.generateId(); + const userId1 = TestHelper.generateId(); + const now1 = 1234; + + state = typingReducer( + state, + { + type: WebsocketEvents.TYPING, + data: { + id: id1, + userId: userId1, + now: now1, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId1]: now1, + }, + }, + 'first user typing', + ); + + const id2 = TestHelper.generateId(); + const now2 = 1235; + + state = typingReducer( + state, + { + type: WebsocketEvents.TYPING, + data: { + id: id2, + userId: userId1, + now: now2, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId1]: now1, + }, + [id2]: { + [userId1]: now2, + }, + }, + 'user typing in second channel', + ); + + const userId2 = TestHelper.generateId(); + const now3 = 1237; + + state = typingReducer( + state, + { + type: WebsocketEvents.TYPING, + data: { + id: id1, + userId: userId2, + now: now3, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId1]: now1, + [userId2]: now3, + }, + [id2]: { + [userId1]: now2, + }, + }, + 'second user typing in channel', + ); + + const now4 = 1238; + + state = typingReducer( + state, + { + type: WebsocketEvents.TYPING, + data: { + id: id2, + userId: userId2, + now: now4, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId1]: now1, + [userId2]: now3, + }, + [id2]: { + [userId1]: now2, + [userId2]: now4, + }, + }, + 'second user typing in second channel', + ); + }); + + it('WebsocketEvents.STOP_TYPING', async () => { + const id1 = TestHelper.generateId(); + const id2 = TestHelper.generateId(); + + const userId1 = TestHelper.generateId(); + const userId2 = TestHelper.generateId(); + + const now1 = 1234; + const now2 = 1235; + const now3 = 1236; + const now4 = 1237; + + let state = { + [id1]: { + [userId1]: now1, + [userId2]: now3, + }, + [id2]: { + [userId1]: now2, + [userId2]: now4, + }, + }; + + state = typingReducer( + state, + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: id1, + userId: userId1, + now: now1, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId2]: now3, + }, + [id2]: { + [userId1]: now2, + [userId2]: now4, + }, + }, + 'deleting first user from first channel', + ); + + state = typingReducer( + state, + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: id2, + userId: userId1, + now: now2, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId2]: now3, + }, + [id2]: { + [userId2]: now4, + }, + }, + 'deleting first user from second channel', + ); + + state = typingReducer( + state, + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: id1, + userId: userId2, + now: now3, + }, + }, + ); + assert.deepEqual( + state, + { + [id2]: { + [userId2]: now4, + }, + }, + 'deleting second user from first channel', + ); + + state = typingReducer( + state, + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: id2, + userId: userId2, + now: now4, + }, + }, + ); + assert.deepEqual( + state, + {}, + 'deleting second user from second channel', + ); + + state = { + [id1]: { + [userId1]: now2, + }, + }; + state = typingReducer( + state, + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: id1, + userId: userId1, + now: now1, + }, + }, + ); + assert.deepEqual( + state, + { + [id1]: { + [userId1]: now2, + }, + }, + 'shouldn\'t delete when the timestamp is older', + ); + + state = typingReducer( + state, + { + type: WebsocketEvents.STOP_TYPING, + data: { + id: id1, + userId: userId1, + now: now3, + }, + }, + ); + assert.deepEqual( + state, + {}, + 'should delete when the timestamp is newer', + ); + }); +}); diff --git a/app/mm-redux/reducers/entities/typing.ts b/app/mm-redux/reducers/entities/typing.ts new file mode 100644 index 000000000..bccccf155 --- /dev/null +++ b/app/mm-redux/reducers/entities/typing.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {WebsocketEvents} from '../../constants'; +import {Typing} from '@mm-redux/types/typing'; +import {GenericAction} from '@mm-redux/types/actions'; +export default function typing(state: Typing = {}, action: GenericAction): Typing { + const { + data, + type, + } = action; + + switch (type) { + case WebsocketEvents.TYPING: { + const { + id, + userId, + now, + } = data; + + if (id && userId) { + return { + ...state, + [id]: { + ...(state[id] || {}), + [userId]: now, + }, + }; + } + + return state; + } + case WebsocketEvents.STOP_TYPING: { + const { + id, + userId, + now, + } = data; + + if (state[id] && state[id][userId] <= now) { + const nextState: Typing = { + ...state, + [id]: {...state[id]}, + }; + + Reflect.deleteProperty(nextState[id], userId); + + if (Object.keys(nextState[id]).length === 0) { + Reflect.deleteProperty(nextState, id); + } + + return nextState; + } + + return state; + } + + default: + return state; + } +} diff --git a/app/mm-redux/reducers/entities/users.test.js b/app/mm-redux/reducers/entities/users.test.js new file mode 100644 index 000000000..d3ffd582f --- /dev/null +++ b/app/mm-redux/reducers/entities/users.test.js @@ -0,0 +1,567 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {UserTypes, ChannelTypes} from '@mm-redux/action_types'; +import reducer from '@mm-redux/reducers/entities/users'; + +describe('Reducers.users', () => { + describe('profilesInChannel', () => { + it('initial state', () => { + const state = undefined; + const action = {}; + const expectedState = { + profilesInChannel: {}, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_IN_CHANNEL, no existing profiles', () => { + const state = { + profilesInChannel: {}, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: { + id: 'id', + user_id: 'user_id', + }, + }; + const expectedState = { + profilesInChannel: { + id: new Set().add('user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_IN_CHANNEL, existing profiles', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: { + id: 'id', + user_id: 'user_id', + }, + }; + const expectedState = { + profilesInChannel: { + id: new Set().add('old_user_id').add('user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, no existing profiles', () => { + const state = { + profilesInChannel: {}, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, + id: 'id', + data: [ + { + id: 'user_id', + }, + { + id: 'user_id_2', + }, + ], + }; + const expectedState = { + profilesInChannel: { + id: new Set().add('user_id').add('user_id_2'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, existing profiles', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, + id: 'id', + data: [ + { + id: 'user_id', + }, + { + id: 'user_id_2', + }, + ], + }; + const expectedState = { + profilesInChannel: { + id: new Set().add('old_user_id').add('user_id').add('user_id_2'), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_IN_CHANNEL, no existing profiles', () => { + const state = { + profilesInChannel: {}, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_IN_CHANNEL, + id: 'id', + data: { + user_id: { + id: 'user_id', + }, + user_id_2: { + id: 'user_id_2', + }, + }, + }; + const expectedState = { + profilesInChannel: { + id: new Set().add('user_id').add('user_id_2'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_IN_CHANNEL, existing profiles', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_IN_CHANNEL, + id: 'id', + data: { + user_id: { + id: 'user_id', + }, + user_id_2: { + id: 'user_id_2', + }, + }, + }; + const expectedState = { + profilesInChannel: { + id: new Set().add('old_user_id').add('user_id').add('user_id_2'), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, unknown user id', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, + data: { + id: 'id', + user_id: 'unknkown_user_id', + }, + }; + const expectedState = state; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, known user id', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, + data: { + id: 'id', + user_id: 'old_user_id', + }, + }; + const expectedState = { + profilesInChannel: { + id: new Set(), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('ChannelTypes.CHANNEL_MEMBER_REMOVED, unknown user id', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: ChannelTypes.CHANNEL_MEMBER_REMOVED, + data: { + channel_id: 'id', + user_id: 'unknkown_user_id', + }, + }; + const expectedState = state; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('ChannelTypes.CHANNEL_MEMBER_REMOVED, known user id', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: ChannelTypes.CHANNEL_MEMBER_REMOVED, + data: { + channel_id: 'id', + user_id: 'old_user_id', + }, + }; + const expectedState = { + profilesInChannel: { + id: new Set(), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + + it('UserTypes.LOGOUT_SUCCESS, existing profiles', () => { + const state = { + profilesInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.LOGOUT_SUCCESS, + }; + const expectedState = { + profilesInChannel: {}, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel); + }); + }); + + describe('profilesNotInChannel', () => { + it('initial state', () => { + const state = undefined; + const action = {}; + const expectedState = { + profilesNotInChannel: {}, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, no existing profiles', () => { + const state = { + profilesNotInChannel: {}, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, + data: { + id: 'id', + user_id: 'user_id', + }, + }; + const expectedState = { + profilesNotInChannel: { + id: new Set().add('user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, existing profiles', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, + data: { + id: 'id', + user_id: 'user_id', + }, + }; + const expectedState = { + profilesNotInChannel: { + id: new Set().add('old_user_id').add('user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL, no existing profiles', () => { + const state = { + profilesNotInChannel: {}, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL, + id: 'id', + data: [ + { + id: 'user_id', + }, + { + id: 'user_id_2', + }, + ], + }; + const expectedState = { + profilesNotInChannel: { + id: new Set().add('user_id').add('user_id_2'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL, existing profiles', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL, + id: 'id', + data: [ + { + id: 'user_id', + }, + { + id: 'user_id_2', + }, + ], + }; + const expectedState = { + profilesNotInChannel: { + id: new Set().add('old_user_id').add('user_id').add('user_id_2'), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL, no existing profiles', () => { + const state = { + profilesNotInChannel: {}, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL, + id: 'id', + data: { + user_id: { + id: 'user_id', + }, + user_id_2: { + id: 'user_id_2', + }, + }, + }; + const expectedState = { + profilesNotInChannel: { + id: new Set().add('user_id').add('user_id_2'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL, existing profiles', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL, + id: 'id', + data: { + user_id: { + id: 'user_id', + }, + user_id_2: { + id: 'user_id_2', + }, + }, + }; + const expectedState = { + profilesNotInChannel: { + id: new Set().add('old_user_id').add('user_id').add('user_id_2'), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_IN_CHANNEL, unknown user id', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: { + id: 'id', + user_id: 'unknkown_user_id', + }, + }; + const expectedState = state; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.RECEIVED_PROFILE_IN_CHANNEL, known user id', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: { + id: 'id', + user_id: 'old_user_id', + }, + }; + const expectedState = { + profilesNotInChannel: { + id: new Set(), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('ChannelTypes.CHANNEL_MEMBER_ADDED, unknown user id', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: ChannelTypes.CHANNEL_MEMBER_ADDED, + data: { + channel_id: 'id', + user_id: 'unknkown_user_id', + }, + }; + const expectedState = state; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('ChannelTypes.CHANNEL_MEMBER_ADDED, known user id', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: ChannelTypes.CHANNEL_MEMBER_ADDED, + data: { + channel_id: 'id', + user_id: 'old_user_id', + }, + }; + const expectedState = { + profilesNotInChannel: { + id: new Set(), + other_id: new Set().add('other_user_id'), + }, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + + it('UserTypes.LOGOUT_SUCCESS, existing profiles', () => { + const state = { + profilesNotInChannel: { + id: new Set().add('old_user_id'), + other_id: new Set().add('other_user_id'), + }, + }; + const action = { + type: UserTypes.LOGOUT_SUCCESS, + }; + const expectedState = { + profilesNotInChannel: {}, + }; + + const newState = reducer(state, action); + assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel); + }); + }); +}); diff --git a/app/mm-redux/reducers/entities/users.ts b/app/mm-redux/reducers/entities/users.ts new file mode 100644 index 000000000..210274a02 --- /dev/null +++ b/app/mm-redux/reducers/entities/users.ts @@ -0,0 +1,606 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; +import {UserTypes, ChannelTypes} from '@mm-redux/action_types'; +import {profileListToMap} from '@mm-redux/utils/user_utils'; +import {GenericAction} from '@mm-redux/types/actions'; +import {UserProfile, UserStatus} from '@mm-redux/types/users'; +import {RelationOneToMany, IDMappedObjects, RelationOneToOne} from '@mm-redux/types/utilities'; +import {Team} from '@mm-redux/types/teams'; +import {Channel} from '@mm-redux/types/channels'; + +function profilesToSet(state: RelationOneToMany, action: GenericAction) { + const id = action.id; + const nextSet = new Set(state[id]); + Object.keys(action.data).forEach((key) => { + nextSet.add(key); + }); + + return { + ...state, + [id]: nextSet, + }; +} + +function profileListToSet(state: RelationOneToMany, action: GenericAction, replace = false) { + const id = action.id; + const nextSet = replace ? new Set() : new Set(state[id]); + if (action.data) { + action.data.forEach((profile: UserProfile) => { + nextSet.add(profile.id); + }); + + return { + ...state, + [id]: nextSet, + }; + } + + return state; +} + +function removeProfileListFromSet(state: RelationOneToMany, action: GenericAction) { + const id = action.id; + const nextSet = new Set(state[id]); + if (action.data) { + action.data.forEach((profile: UserProfile) => { + nextSet.delete(profile.id); + }); + + return { + ...state, + [id]: nextSet, + }; + } + + return state; +} + +function addProfileToSet(state: RelationOneToMany, action: GenericAction) { + const {id, user_id: userId} = action.data; + const nextSet = new Set(state[id]); + nextSet.add(userId); + return { + ...state, + [id]: nextSet, + }; +} + +function removeProfileFromTeams(state: RelationOneToMany, action: GenericAction) { + const newState = {...state}; + let removed = false; + Object.keys(state).forEach((key) => { + if (newState[key][action.data.user_id]) { + delete newState[key][action.data.user_id]; + removed = true; + } + }); + return removed ? newState : state; +} + +function removeProfileFromSet(state: RelationOneToMany, action: GenericAction) { + const {id, user_id: userId} = action.data; + const nextSet = new Set(state[id]); + nextSet.delete(userId); + return { + ...state, + [id]: nextSet, + }; +} + +function currentUserId(state = '', action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_ME: { + const data = action.data || action.payload; + + return data.id; + } + + case UserTypes.LOGIN: { // Used by the mobile app + const {user} = action.data; + + return user ? user.id : state; + } + case UserTypes.LOGOUT_SUCCESS: + return ''; + } + + return state; +} + +function mySessions(state: Array<{id: string}> = [], action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_SESSIONS: + return [...action.data]; + + case UserTypes.RECEIVED_REVOKED_SESSION: { + let index = -1; + const length = state.length; + for (let i = 0; i < length; i++) { + if (state[i].id === action.sessionId) { + index = i; + break; + } + } + if (index > -1) { + return state.slice(0, index).concat(state.slice(index + 1)); + } + + return state; + } + + case UserTypes.REVOKE_ALL_USER_SESSIONS_SUCCESS: + if (action.data.isCurrentUser === true) { + return []; + } + return state; + + case UserTypes.REVOKE_SESSIONS_FOR_ALL_USERS_SUCCESS: + return []; + + case UserTypes.LOGOUT_SUCCESS: + return []; + + default: + return state; + } +} + +function myAudits(state = [], action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_AUDITS: + return [...action.data]; + + case UserTypes.LOGOUT_SUCCESS: + return []; + + default: + return state; + } +} + +function profiles(state: IDMappedObjects = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_ME: + case UserTypes.RECEIVED_PROFILE: { + const data = action.data || action.payload; + const user = {...data}; + const oldUser = state[data.id]; + if (oldUser) { + user.terms_of_service_id = oldUser.terms_of_service_id; + user.terms_of_service_create_at = oldUser.terms_of_service_create_at; + } + + return { + ...state, + [data.id]: user, + }; + } + case UserTypes.RECEIVED_PROFILES_LIST: + return Object.assign({}, state, profileListToMap(action.data)); + case UserTypes.RECEIVED_PROFILES: + return Object.assign({}, state, action.data); + + case UserTypes.LOGIN: { // Used by the mobile app + const {user} = action.data; + + if (user) { + const nextState = {...state}; + nextState[user.id] = user; + return nextState; + } + + return state; + } + case UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL: { + const {data} = action; + if (data && data.length) { + const nextState = {...state}; + const ids = new Set(); + + data.forEach((d: any) => { + d.data.users.forEach((u: UserProfile) => { + if (!ids.has(u.id)) { + ids.add(u.id); + nextState[u.id] = u; + } + }); + }); + return nextState; + } + + return state; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + case UserTypes.RECEIVED_TERMS_OF_SERVICE_STATUS: { + const data = action.data || action.payload; + return { + ...state, + [data.user_id]: { + ...state[data.user_id], + terms_of_service_id: data.terms_of_service_id, + terms_of_service_create_at: data.terms_of_service_create_at, + }, + }; + } + case UserTypes.PROFILE_NO_LONGER_VISIBLE: { + if (state[action.data.user_id]) { + const newState = {...state}; + delete newState[action.data.user_id]; + return newState; + } + return state; + } + default: + return state; + } +} + +function profilesInTeam(state: RelationOneToMany = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_PROFILE_IN_TEAM: + return addProfileToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM: + return profileListToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_IN_TEAM: + return profilesToSet(state, action); + + case UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM: + return removeProfileFromSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM: + return removeProfileListFromSet(state, action); + + case UserTypes.LOGOUT_SUCCESS: + return {}; + + case UserTypes.PROFILE_NO_LONGER_VISIBLE: + return removeProfileFromTeams(state, action); + + default: + return state; + } +} + +function profilesNotInTeam(state: RelationOneToMany = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM: + return addProfileToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM: + return profileListToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM_AND_REPLACE: + return profileListToSet(state, action, true); + + case UserTypes.RECEIVED_PROFILE_IN_TEAM: + return removeProfileFromSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM: + return removeProfileListFromSet(state, action); + + case UserTypes.LOGOUT_SUCCESS: + return {}; + + case UserTypes.PROFILE_NO_LONGER_VISIBLE: + return removeProfileFromTeams(state, action); + + default: + return state; + } +} + +function profilesWithoutTeam(state: Set = new Set(), action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_PROFILE_WITHOUT_TEAM: { + const nextSet = new Set(state); + Object.values(action.data).forEach((id: string) => nextSet.add(id)); + return nextSet; + } + case UserTypes.RECEIVED_PROFILES_LIST_WITHOUT_TEAM: { + const nextSet = new Set(state); + action.data.forEach((user: UserProfile) => nextSet.add(user.id)); + return nextSet; + } + case UserTypes.PROFILE_NO_LONGER_VISIBLE: + case UserTypes.RECEIVED_PROFILE_IN_TEAM: { + const nextSet = new Set(state); + nextSet.delete(action.data.id); + return nextSet; + } + case UserTypes.LOGOUT_SUCCESS: + return new Set(); + + default: + return state; + } +} + +function profilesInChannel(state: RelationOneToMany = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_PROFILE_IN_CHANNEL: + return addProfileToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL: + return profileListToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_IN_CHANNEL: + return profilesToSet(state, action); + + case UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL: + return removeProfileFromSet(state, action); + + case ChannelTypes.CHANNEL_MEMBER_REMOVED: + return removeProfileFromSet(state, { + type: '', + data: { + id: action.data.channel_id, + user_id: action.data.user_id, + }}); + + case UserTypes.LOGOUT_SUCCESS: + return {}; + + case UserTypes.PROFILE_NO_LONGER_VISIBLE: + return removeProfileFromTeams(state, action); + + case UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL: { // Used by the mobile app + const {data} = action; + + if (data && data.length) { + const nextState: any = {...state}; + data.forEach((d: any) => { + const {channelId, users} = d.data; + const nextSet = new Set(state[channelId]); + + users.forEach((u: UserProfile) => nextSet.add(u.id)); + nextState[channelId] = nextSet; + }); + + return nextState; + } + + return state; + } + default: + return state; + } +} + +function profilesNotInChannel(state: RelationOneToMany = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL: + return addProfileToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL: + return profileListToSet(state, action); + + case UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_CHANNEL_AND_REPLACE: + return profileListToSet(state, action, true); + + case UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL: + return profilesToSet(state, action); + + case UserTypes.RECEIVED_PROFILE_IN_CHANNEL: + return removeProfileFromSet(state, action); + + case ChannelTypes.CHANNEL_MEMBER_ADDED: + return removeProfileFromSet(state, { + type: '', + data: { + id: action.data.channel_id, + user_id: action.data.user_id, + }}); + + case UserTypes.LOGOUT_SUCCESS: + return {}; + + case UserTypes.PROFILE_NO_LONGER_VISIBLE: + return removeProfileFromTeams(state, action); + + default: + return state; + } +} + +function statuses(state: RelationOneToOne = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_STATUS: { + const nextState = Object.assign({}, state); + nextState[action.data.user_id] = action.data.status; + + return nextState; + } + case UserTypes.RECEIVED_STATUSES: { + const nextState = Object.assign({}, state); + + for (const s of action.data) { + nextState[s.user_id] = s.status; + } + + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + case UserTypes.PROFILE_NO_LONGER_VISIBLE: { + if (state[action.data.user_id]) { + const newState = {...state}; + delete newState[action.data.user_id]; + return newState; + } + return state; + } + case UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL: { // Used by the mobile app + const {data} = action; + if (data && data.length) { + const nextState = {...state}; + const ids = new Set(); + + let hasNewStatuses = false; + data.forEach((d: any) => { + const {statuses: st} = d.data; + if (st && st.length) { + st.forEach((u: UserStatus) => { + if (!ids.has(u.user_id)) { + ids.add(u.user_id); + nextState[u.user_id] = u.status; + hasNewStatuses = true; + } + }); + } + }); + + if (hasNewStatuses) { + return nextState; + } + } + + return state; + } + default: + return state; + } +} + +function isManualStatus(state: RelationOneToOne = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_STATUS: { + const nextState = Object.assign({}, state); + nextState[action.data.user_id] = action.data.manual; + + return nextState; + } + case UserTypes.RECEIVED_STATUSES: { + const nextState = Object.assign({}, state); + + for (const s of action.data) { + nextState[s.user_id] = s.manual; + } + + return nextState; + } + case UserTypes.LOGOUT_SUCCESS: + return {}; + case UserTypes.PROFILE_NO_LONGER_VISIBLE: { + if (state[action.data.user_id]) { + const newState = {...state}; + delete newState[action.data.user_id]; + return newState; + } + return state; + } + default: + return state; + } +} + +function myUserAccessTokens(state: any = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_MY_USER_ACCESS_TOKEN: { + const nextState = {...state}; + nextState[action.data.id] = action.data; + + return nextState; + } + case UserTypes.RECEIVED_MY_USER_ACCESS_TOKENS: { + const nextState = {...state}; + + for (const uat of action.data) { + nextState[uat.id] = uat; + } + + return nextState; + } + case UserTypes.REVOKED_USER_ACCESS_TOKEN: { + const nextState = {...state}; + Reflect.deleteProperty(nextState, action.data); + + return nextState; + } + + case UserTypes.ENABLED_USER_ACCESS_TOKEN: { + if (state[action.data]) { + const nextState = {...state}; + nextState[action.data] = {...nextState[action.data], is_active: true}; + return nextState; + } + return state; + } + + case UserTypes.DISABLED_USER_ACCESS_TOKEN: { + if (state[action.data]) { + const nextState = {...state}; + nextState[action.data] = {...nextState[action.data], is_active: false}; + return nextState; + } + return state; + } + + case UserTypes.CLEAR_MY_USER_ACCESS_TOKENS: + case UserTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + +function stats(state = {}, action: GenericAction) { + switch (action.type) { + case UserTypes.RECEIVED_USER_STATS: { + const stat = action.data; + return { + ...state, + ...stat, + }; + } + default: + return state; + } +} + +export default combineReducers({ + + // the current selected user + currentUserId, + + // array with the user's sessions + mySessions, + + // array with the user's audits + myAudits, + + // object where every key is the token id and has a user access token as a value + myUserAccessTokens, + + // object where every key is a user id and has an object with the users details + profiles, + + // object where every key is a team id and has a Set with the users id that are members of the team + profilesInTeam, + + // object where every key is a team id and has a Set with the users id that are not members of the team + profilesNotInTeam, + + // set with user ids for users that are not on any team + profilesWithoutTeam, + + // object where every key is a channel id and has a Set with the users id that are members of the channel + profilesInChannel, + + // object where every key is a channel id and has a Set with the users id that are not members of the channel + profilesNotInChannel, + + // object where every key is the user id and has a value with the current status of each user + statuses, + + // object where every key is the user id and has a value with a flag determining if their status was set manually + isManualStatus, + + // Total user stats + stats, +}); diff --git a/app/mm-redux/reducers/errors/index.ts b/app/mm-redux/reducers/errors/index.ts new file mode 100644 index 000000000..168df9a42 --- /dev/null +++ b/app/mm-redux/reducers/errors/index.ts @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {ErrorTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; +export default ((state: Array<{error: any;displayable?: boolean;date: string}> = [], action: GenericAction) => { + switch (action.type) { + case ErrorTypes.DISMISS_ERROR: { + const nextState = [...state]; + nextState.splice(action.index!, 1); + + return nextState; + } + case ErrorTypes.LOG_ERROR: { + const nextState = [...state]; + const {displayable, error} = action; + nextState.push({ + displayable, + error, + date: new Date(Date.now()).toUTCString(), + }); + + return nextState; + } + case ErrorTypes.RESTORE_ERRORS: + return action.data; + case ErrorTypes.CLEAR_ERRORS: { + return []; + } + default: + return state; + } +}); diff --git a/app/mm-redux/reducers/index.ts b/app/mm-redux/reducers/index.ts new file mode 100644 index 000000000..936a290e8 --- /dev/null +++ b/app/mm-redux/reducers/index.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import entities from './entities'; +import errors from './errors'; +import requests from './requests'; +import websocket from './websocket'; + +export default { + entities, + errors, + requests, + websocket, +}; diff --git a/app/mm-redux/reducers/requests/channels.ts b/app/mm-redux/reducers/requests/channels.ts new file mode 100644 index 000000000..e2626c23d --- /dev/null +++ b/app/mm-redux/reducers/requests/channels.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {ChannelTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {ChannelsRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function myChannels(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + ChannelTypes.CHANNELS_REQUEST, + ChannelTypes.CHANNELS_SUCCESS, + ChannelTypes.CHANNELS_FAILURE, + state, + action + ); +} + +function createChannel(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + ChannelTypes.CREATE_CHANNEL_REQUEST, + ChannelTypes.CREATE_CHANNEL_SUCCESS, + ChannelTypes.CREATE_CHANNEL_FAILURE, + state, + action + ); +} + +function updateChannel(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + ChannelTypes.UPDATE_CHANNEL_REQUEST, + ChannelTypes.UPDATE_CHANNEL_SUCCESS, + ChannelTypes.UPDATE_CHANNEL_FAILURE, + state, + action + ); +} + +function getChannels(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + ChannelTypes.GET_CHANNELS_REQUEST, + ChannelTypes.GET_CHANNELS_SUCCESS, + ChannelTypes.GET_CHANNELS_FAILURE, + state, + action + ); +} + +function getAllChannels(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + ChannelTypes.GET_ALL_CHANNELS_REQUEST, + ChannelTypes.GET_ALL_CHANNELS_SUCCESS, + ChannelTypes.GET_ALL_CHANNELS_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + getChannels, + getAllChannels, + myChannels, + createChannel, + updateChannel, +}) as (b: ChannelsRequestsStatuses, a: GenericAction) => ChannelsRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/files.ts b/app/mm-redux/reducers/requests/files.ts new file mode 100644 index 000000000..15181cf8d --- /dev/null +++ b/app/mm-redux/reducers/requests/files.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {FileTypes} from '@mm-redux/action_types'; +import {RequestStatus} from '../../constants'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {FilesRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {initialRequestState} from './helpers'; + +export function handleUploadFilesRequest( + REQUEST: string, + SUCCESS: string, + FAILURE: string, + CANCEL: string, + state: RequestStatusType, + action: GenericAction +): RequestStatusType { + switch (action.type) { + case REQUEST: + return { + ...state, + status: RequestStatus.STARTED, + }; + case SUCCESS: + return { + ...state, + status: RequestStatus.SUCCESS, + error: null, + }; + case FAILURE: { + let error = action.error; + + if (error instanceof Error) { + error = error.hasOwnProperty('intl') ? {...error} : error.toString(); + } + + return { + ...state, + status: RequestStatus.FAILURE, + error, + }; + } + case CANCEL: + return { + ...state, + status: RequestStatus.CANCELLED, + error: null, + }; + default: + return state; + } +} + +function uploadFiles(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleUploadFilesRequest( + FileTypes.UPLOAD_FILES_REQUEST, + FileTypes.UPLOAD_FILES_SUCCESS, + FileTypes.UPLOAD_FILES_FAILURE, + FileTypes.UPLOAD_FILES_CANCEL, + state, + action + ); +} + +export default (combineReducers({ + uploadFiles, +}) as (b: FilesRequestsStatuses, a: GenericAction) => FilesRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/general.ts b/app/mm-redux/reducers/requests/general.ts new file mode 100644 index 000000000..84674ff28 --- /dev/null +++ b/app/mm-redux/reducers/requests/general.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {GeneralTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {GeneralRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function websocket(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + if (action.type === GeneralTypes.WEBSOCKET_CLOSED) { + return initialRequestState(); + } + + return handleRequest( + GeneralTypes.WEBSOCKET_REQUEST, + GeneralTypes.WEBSOCKET_SUCCESS, + GeneralTypes.WEBSOCKET_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + websocket, +}) as (b: GeneralRequestsStatuses, a: GenericAction) => GeneralRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/helpers.ts b/app/mm-redux/reducers/requests/helpers.ts new file mode 100644 index 000000000..739e9beed --- /dev/null +++ b/app/mm-redux/reducers/requests/helpers.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {RequestStatus} from '../../constants'; +import {GenericAction} from '@mm-redux/types/actions'; +import {RequestStatusType} from '@mm-redux/types/requests'; +export function initialRequestState(): RequestStatusType { + return { + status: RequestStatus.NOT_STARTED, + error: null, + }; +} + +export function handleRequest( + REQUEST: string, + SUCCESS: string, + FAILURE: string, + state: RequestStatusType, + action: GenericAction +): RequestStatusType { + switch (action.type) { + case REQUEST: + return { + ...state, + status: RequestStatus.STARTED, + }; + case SUCCESS: + return { + ...state, + status: RequestStatus.SUCCESS, + error: null, + }; + case FAILURE: { + return { + ...state, + status: RequestStatus.FAILURE, + error: action.error, + }; + } + default: + return state; + } +} diff --git a/app/mm-redux/reducers/requests/index.ts b/app/mm-redux/reducers/requests/index.ts new file mode 100644 index 000000000..1f78e2aa2 --- /dev/null +++ b/app/mm-redux/reducers/requests/index.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; + +import channels from './channels'; +import files from './files'; +import general from './general'; +import posts from './posts'; +import teams from './teams'; +import users from './users'; +import jobs from './jobs'; +import search from './search'; +import roles from './roles'; + +export default combineReducers({ + channels, + files, + general, + posts, + teams, + users, + jobs, + search, + roles, +}); diff --git a/app/mm-redux/reducers/requests/jobs.ts b/app/mm-redux/reducers/requests/jobs.ts new file mode 100644 index 000000000..82bc88a95 --- /dev/null +++ b/app/mm-redux/reducers/requests/jobs.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {JobTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {JobsRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function createJob(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + JobTypes.CREATE_JOB_REQUEST, + JobTypes.CREATE_JOB_SUCCESS, + JobTypes.CREATE_JOB_FAILURE, + state, + action + ); +} + +function getJob(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + JobTypes.GET_JOB_REQUEST, + JobTypes.GET_JOB_SUCCESS, + JobTypes.GET_JOB_FAILURE, + state, + action + ); +} + +function getJobs(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + JobTypes.GET_JOBS_REQUEST, + JobTypes.GET_JOBS_SUCCESS, + JobTypes.GET_JOBS_FAILURE, + state, + action + ); +} + +function cancelJob(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + JobTypes.CANCEL_JOB_REQUEST, + JobTypes.CANCEL_JOB_SUCCESS, + JobTypes.CANCEL_JOB_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + createJob, + getJob, + getJobs, + cancelJob, +}) as (b: JobsRequestsStatuses, a: GenericAction) => JobsRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/posts.ts b/app/mm-redux/reducers/requests/posts.ts new file mode 100644 index 000000000..80b2bacf9 --- /dev/null +++ b/app/mm-redux/reducers/requests/posts.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {PostTypes} from '@mm-redux/action_types'; +import {handleRequest, initialRequestState} from './helpers'; +import {GenericAction} from '@mm-redux/types/actions'; +import {PostsRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +function createPost(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + if (action.type === PostTypes.CREATE_POST_RESET_REQUEST) { + return initialRequestState(); + } + + return handleRequest( + PostTypes.CREATE_POST_REQUEST, + PostTypes.CREATE_POST_SUCCESS, + PostTypes.CREATE_POST_FAILURE, + state, + action + ); +} + +function editPost(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + PostTypes.EDIT_POST_REQUEST, + PostTypes.EDIT_POST_SUCCESS, + PostTypes.EDIT_POST_FAILURE, + state, + action + ); +} + +function getPostThread(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + PostTypes.GET_POST_THREAD_REQUEST, + PostTypes.GET_POST_THREAD_SUCCESS, + PostTypes.GET_POST_THREAD_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + createPost, + editPost, + getPostThread, +}) as (b: PostsRequestsStatuses, a: GenericAction) => PostsRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/roles.ts b/app/mm-redux/reducers/requests/roles.ts new file mode 100644 index 000000000..0d4a7b7d2 --- /dev/null +++ b/app/mm-redux/reducers/requests/roles.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {RoleTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {RolesRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function getRolesByNames(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + RoleTypes.ROLES_BY_NAMES_REQUEST, + RoleTypes.ROLES_BY_NAMES_SUCCESS, + RoleTypes.ROLES_BY_NAMES_FAILURE, + state, + action + ); +} + +function getRoleByName(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + RoleTypes.ROLE_BY_NAME_REQUEST, + RoleTypes.ROLE_BY_NAME_SUCCESS, + RoleTypes.ROLE_BY_NAME_FAILURE, + state, + action + ); +} + +function getRole(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + RoleTypes.ROLE_BY_ID_REQUEST, + RoleTypes.ROLE_BY_ID_SUCCESS, + RoleTypes.ROLE_BY_ID_FAILURE, + state, + action + ); +} + +function editRole(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + RoleTypes.EDIT_ROLE_REQUEST, + RoleTypes.EDIT_ROLE_SUCCESS, + RoleTypes.EDIT_ROLE_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + getRolesByNames, + getRoleByName, + getRole, + editRole, +}) as (b: RolesRequestsStatuses, a: GenericAction) => RolesRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/search.ts b/app/mm-redux/reducers/requests/search.ts new file mode 100644 index 000000000..fe586a75d --- /dev/null +++ b/app/mm-redux/reducers/requests/search.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {SearchTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {SearchRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function flaggedPosts(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + if (action.type === SearchTypes.REMOVE_SEARCH_POSTS) { + return initialRequestState(); + } + + return handleRequest( + SearchTypes.SEARCH_FLAGGED_POSTS_REQUEST, + SearchTypes.SEARCH_FLAGGED_POSTS_SUCCESS, + SearchTypes.SEARCH_FLAGGED_POSTS_FAILURE, + state, + action + ); +} + +function pinnedPosts(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + if (action.type === SearchTypes.REMOVE_SEARCH_POSTS) { + return initialRequestState(); + } + + return handleRequest( + SearchTypes.SEARCH_PINNED_POSTS_REQUEST, + SearchTypes.SEARCH_PINNED_POSTS_SUCCESS, + SearchTypes.SEARCH_PINNED_POSTS_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + flaggedPosts, + pinnedPosts, +}) as (b: SearchRequestsStatuses, a: GenericAction) => SearchRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/teams.ts b/app/mm-redux/reducers/requests/teams.ts new file mode 100644 index 000000000..54aa8f74d --- /dev/null +++ b/app/mm-redux/reducers/requests/teams.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {TeamTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {TeamsRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function getMyTeams(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + TeamTypes.MY_TEAMS_REQUEST, + TeamTypes.MY_TEAMS_SUCCESS, + TeamTypes.MY_TEAMS_FAILURE, + state, + action + ); +} + +function getTeams(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + TeamTypes.GET_TEAMS_REQUEST, + TeamTypes.GET_TEAMS_SUCCESS, + TeamTypes.GET_TEAMS_FAILURE, + state, + action + ); +} + +function joinTeam(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + TeamTypes.JOIN_TEAM_REQUEST, + TeamTypes.JOIN_TEAM_SUCCESS, + TeamTypes.JOIN_TEAM_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + getTeams, + getMyTeams, + joinTeam, +}) as (b: TeamsRequestsStatuses, a: GenericAction) => TeamsRequestsStatuses); diff --git a/app/mm-redux/reducers/requests/users.ts b/app/mm-redux/reducers/requests/users.ts new file mode 100644 index 000000000..c90ca486f --- /dev/null +++ b/app/mm-redux/reducers/requests/users.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {RequestStatus} from '../../constants'; +import {UserTypes} from '@mm-redux/action_types'; + +import {GenericAction} from '@mm-redux/types/actions'; +import {UsersRequestsStatuses, RequestStatusType} from '@mm-redux/types/requests'; + +import {handleRequest, initialRequestState} from './helpers'; + +function checkMfa(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + switch (action.type) { + case UserTypes.CHECK_MFA_REQUEST: + return {...state, status: RequestStatus.STARTED}; + + case UserTypes.CHECK_MFA_SUCCESS: + return {...state, status: RequestStatus.SUCCESS, error: null}; + + case UserTypes.CHECK_MFA_FAILURE: + return {...state, status: RequestStatus.FAILURE, error: action.error}; + + case UserTypes.LOGOUT_SUCCESS: + return {...state, status: RequestStatus.NOT_STARTED, error: null}; + + default: + return state; + } +} + +function login(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + switch (action.type) { + case UserTypes.LOGIN_REQUEST: + return {...state, status: RequestStatus.STARTED}; + + case UserTypes.LOGIN_SUCCESS: + return {...state, status: RequestStatus.SUCCESS, error: null}; + + case UserTypes.LOGIN_FAILURE: + return {...state, status: RequestStatus.FAILURE, error: action.error}; + + case UserTypes.LOGOUT_SUCCESS: + return {...state, status: RequestStatus.NOT_STARTED, error: null}; + + default: + return state; + } +} + +function logout(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + switch (action.type) { + case UserTypes.LOGOUT_REQUEST: + return {...state, status: RequestStatus.STARTED}; + + case UserTypes.LOGOUT_SUCCESS: + return {...state, status: RequestStatus.SUCCESS, error: null}; + + case UserTypes.LOGOUT_FAILURE: + return {...state, status: RequestStatus.FAILURE, error: action.error}; + + case UserTypes.RESET_LOGOUT_STATE: + return initialRequestState(); + + default: + return state; + } +} + +function autocompleteUsers(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + UserTypes.AUTOCOMPLETE_USERS_REQUEST, + UserTypes.AUTOCOMPLETE_USERS_SUCCESS, + UserTypes.AUTOCOMPLETE_USERS_FAILURE, + state, + action + ); +} + +function updateMe(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType { + return handleRequest( + UserTypes.UPDATE_ME_REQUEST, + UserTypes.UPDATE_ME_SUCCESS, + UserTypes.UPDATE_ME_FAILURE, + state, + action + ); +} + +export default (combineReducers({ + checkMfa, + login, + logout, + autocompleteUsers, + updateMe, +}) as (b: UsersRequestsStatuses, a: GenericAction) => UsersRequestsStatuses); diff --git a/app/mm-redux/reducers/websocket.test.js b/app/mm-redux/reducers/websocket.test.js new file mode 100644 index 000000000..39bd639e6 --- /dev/null +++ b/app/mm-redux/reducers/websocket.test.js @@ -0,0 +1,102 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GeneralTypes} from '@mm-redux/action_types'; + +import reducer from './websocket'; + +describe('websocket', () => { + describe('lastConnectAt', () => { + test('should update lastConnectAt when first connecting', () => { + let state = reducer(undefined, {}); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 1000, + }); + + expect(state.connected).toBe(true); + expect(state.lastConnectAt).toBe(1000); + }); + + test('should not update lastConnectAt when already connected', () => { + let state = reducer(undefined, {}); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 1000, + }); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 2000, + }); + + expect(state.connected).toBe(true); + expect(state.lastConnectAt).toBe(1000); + }); + + test('should update when reconnecting', () => { + let state = reducer(undefined, {}); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 1000, + }); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_FAILURE, + timestamp: 2000, + }); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 3000, + }); + + expect(state.connected).toBe(true); + expect(state.lastConnectAt).toBe(3000); + }); + }); + + describe('lastDisconnectAt', () => { + test('should update lastDisconnectAt when disconnected', () => { + let state = reducer(undefined, {}); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 1000, + }); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_FAILURE, + timestamp: 2000, + }); + + expect(state.connected).toBe(false); + expect(state.lastDisconnectAt).toBe(2000); + }); + + test('should not update lastDisconnectAt when failing to reconnect', () => { + let state = reducer(undefined, {}); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_SUCCESS, + timestamp: 1000, + }); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_FAILURE, + timestamp: 2000, + }); + + state = reducer(state, { + type: GeneralTypes.WEBSOCKET_FAILURE, + timestamp: 3000, + }); + + expect(state.connected).toBe(false); + expect(state.lastDisconnectAt).toBe(2000); + }); + }); +}); diff --git a/app/mm-redux/reducers/websocket.ts b/app/mm-redux/reducers/websocket.ts new file mode 100644 index 000000000..4be89bad2 --- /dev/null +++ b/app/mm-redux/reducers/websocket.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GeneralTypes, UserTypes} from '@mm-redux/action_types'; +import {GenericAction} from '@mm-redux/types/actions'; + +function getInitialState() { + return { + connected: false, + lastConnectAt: 0, + lastDisconnectAt: 0, + }; +} + +export default function(state = getInitialState(), action: GenericAction) { + if (!state.connected && action.type === GeneralTypes.WEBSOCKET_SUCCESS) { + return { + ...state, + connected: true, + lastConnectAt: action.timestamp, + }; + } else if (state.connected && (action.type === GeneralTypes.WEBSOCKET_FAILURE || action.type === GeneralTypes.WEBSOCKET_CLOSED)) { + return { + ...state, + connected: false, + lastDisconnectAt: action.timestamp, + }; + } + + if (action.type === UserTypes.LOGOUT_SUCCESS) { + return getInitialState(); + } + + return state; +} diff --git a/app/mm-redux/selectors/entities/bots.ts b/app/mm-redux/selectors/entities/bots.ts new file mode 100644 index 000000000..ffaeba7ad --- /dev/null +++ b/app/mm-redux/selectors/entities/bots.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GlobalState} from '@mm-redux/types/store'; + +export function getBotAccounts(state: GlobalState) { + return state.entities.bots.accounts; +} diff --git a/app/mm-redux/selectors/entities/channel_categories.test.js b/app/mm-redux/selectors/entities/channel_categories.test.js new file mode 100644 index 000000000..c70e5c295 --- /dev/null +++ b/app/mm-redux/selectors/entities/channel_categories.test.js @@ -0,0 +1,1201 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {General, Preferences} from '../../constants'; +import {CategoryTypes} from '../../constants/channel_categories'; + +import {getCurrentChannelId, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getLastPostPerChannel} from '@mm-redux/selectors/entities/posts'; +import {getMyPreferences} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; + +import {isGroupOrDirectChannelVisible} from '@mm-redux/utils/channel_utils'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; + +import mergeObjects from 'test/merge_objects'; + +import * as Selectors from './channel_categories'; + +describe('makeGetCategoriesForTeam', () => { + const category1 = {id: 'category1', display_name: 'Category One', type: CategoryTypes.CUSTOM}; + const category2 = {id: 'category2', display_name: 'Category Two', type: CategoryTypes.CUSTOM}; + + const state = { + entities: { + channelCategories: { + byId: { + category1, + category2, + }, + orderByTeam: { + team1: [category2.id, category1.id], + }, + }, + }, + }; + + test('should return categories for team in order', () => { + const getCategoriesForTeam = Selectors.makeGetCategoriesForTeam(); + + expect(getCategoriesForTeam(state, 'team1')).toEqual([ + state.entities.channelCategories.byId.category2, + state.entities.channelCategories.byId.category1, + ]); + }); + + test('should memoize properly', () => { + const getCategoriesForTeam = Selectors.makeGetCategoriesForTeam(); + + const result = getCategoriesForTeam(state, 'team1'); + + // Repeat calls should return the same array + expect(getCategoriesForTeam(state, 'team1')).toBe(result); + + // Calls to a difference instance of the selector won't return the same array + expect(result).not.toBe(Selectors.makeGetCategoriesForTeam()(state, 'team1')); + + // Calls with different arguments won't return the same array + expect(getCategoriesForTeam(state, 'team2')).not.toBe(result); + + // Calls after different argumetns won't return the same array + expect(getCategoriesForTeam(state, 'team1')).not.toBe(result); + }); +}); + +describe('makeGetUnsortedUnfilteredChannels', () => { + const channel1 = {id: 'channel1', team_id: 'team1', delete_at: 0}; + const channel2 = {id: 'channel2', team_id: 'team1', delete_at: 0}; + const channel3 = {id: 'channel3', team_id: 'team2', delete_at: 0}; + const dmChannel1 = {id: 'dmChannel1', team_id: '', delete_at: 0}; + const gmChannel1 = {id: 'gmChannel1', team_id: '', delete_at: 0}; + + const baseState = { + entities: { + channels: { + channels: { + channel1, + channel2, + channel3, + dmChannel1, + gmChannel1, + }, + myMembers: { + [channel1.id]: {}, + [channel2.id]: {}, + [channel3.id]: {}, + [dmChannel1.id]: {}, + [gmChannel1.id]: {}, + }, + }, + }, + }; + + test('should return channels on the team and DMs/GMs', () => { + const getUnsortedUnfilteredChannels = Selectors.makeGetUnsortedUnfilteredChannels(); + + expect(getUnsortedUnfilteredChannels(baseState, 'team1')).toMatchObject([channel1, channel2, dmChannel1, gmChannel1]); + + expect(getUnsortedUnfilteredChannels(baseState, 'team2')).toMatchObject([channel3, dmChannel1, gmChannel1]); + }); + + test('should not return channels which the user is not a member of', () => { + const channel4 = {id: 'channel4', team_id: 'team1', delete_at: 0}; + + const getUnsortedUnfilteredChannels = Selectors.makeGetUnsortedUnfilteredChannels(); + + let state = { + entities: { + channels: { + channels: { + channel4, + }, + myMembers: {}, + }, + }, + }; + + expect(getUnsortedUnfilteredChannels(state, 'team1')).not.toContain(channel4); + + state = { + entities: { + channels: { + channels: { + channel4, + }, + myMembers: { + [channel4.id]: {}, + }, + }, + }, + }; + + expect(getUnsortedUnfilteredChannels(state, 'team1')).toContain(channel4); + }); + + test('should not return deleted channels', () => { + const channel = {id: 'channel', team_id: 'team1', delete_at: 0}; + const deletedChannel = {id: 'deletedChannel', team_id: 'team1', delete_at: 1000}; + + const getUnsortedUnfilteredChannels = Selectors.makeGetUnsortedUnfilteredChannels(); + + const state = { + entities: { + channels: { + channels: { + channel, + }, + myMembers: { + [channel.id]: {}, + }, + }, + }, + }; + + expect(getUnsortedUnfilteredChannels(state, 'team1')).toContain(channel); + expect(getUnsortedUnfilteredChannels(state, 'team1')).not.toContain(deletedChannel); + }); + + test('should memoize properly', () => { + const getUnsortedUnfilteredChannels = Selectors.makeGetUnsortedUnfilteredChannels(); + + const result = getUnsortedUnfilteredChannels(baseState, 'team1'); + + // Repeat calls should return the same array + expect(getUnsortedUnfilteredChannels(baseState, 'team1')).toBe(result); + + // Calls to a difference instance of the selector won't return the same array + expect(result).not.toBe(Selectors.makeGetUnsortedUnfilteredChannels()(baseState, 'team1')); + + // Calls with different arguments won't return the same array + expect(getUnsortedUnfilteredChannels(baseState, 'team2')).not.toBe(result); + + // Calls after different argumetns won't return the same array + expect(getUnsortedUnfilteredChannels(baseState, 'team1')).not.toBe(result); + }); +}); + +describe('makeFilterChannelsByFavorites', () => { + const channel1 = {id: 'channel1'}; + const channel2 = {id: 'channel2'}; + + const state = { + entities: { + channels: { + channels: { + channel1, + channel2, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, channel2.id)]: {value: 'false'}, + }, + }, + }, + }; + + const channels = [channel1, channel2]; + + test('should return only favorited channels for the favorites category', () => { + const filterChanneldByFavorites = Selectors.makeFilterChannelsByFavorites(); + + expect(filterChanneldByFavorites(state, channels, CategoryTypes.FAVORITES)).toMatchObject([channel1]); + }); + + test('should not return favorited channels for other categories', () => { + const filterChanneldByFavorites = Selectors.makeFilterChannelsByFavorites(); + + expect(filterChanneldByFavorites(state, channels, CategoryTypes.CUSTOM)).toMatchObject([channel2]); + }); + + test('should memoize properly', () => { + const filterChanneldByFavorites = Selectors.makeFilterChannelsByFavorites(); + + const result = filterChanneldByFavorites(state, channels, CategoryTypes.CUSTOM); + + // Repeat calls should return the same array + expect(filterChanneldByFavorites(state, channels, CategoryTypes.CUSTOM)).toBe(result); + + // Calls to a difference instance of the selector won't return the same array + expect(result).not.toBe(Selectors.makeFilterChannelsByFavorites()(state, channels, CategoryTypes.CUSTOM)); + + // Calls with different arguments won't return the same array + expect(filterChanneldByFavorites(state, [channel1], CategoryTypes.CUSTOM)).not.toBe(result); + + // Calls after different argumetns won't return the same array + expect(filterChanneldByFavorites(state, channels, CategoryTypes.CUSTOM)).not.toBe(result); + }); +}); + +describe('makeFilterChannelsByType', () => { + const channel1 = {id: 'channel1', type: General.OPEN_CHANNEL}; + const channel2 = {id: 'channel2', type: General.PRIVATE_CHANNEL}; + const dmChannel1 = {id: 'dmChannel1', type: General.DM_CHANNEL}; + const gmChannel1 = {id: 'gmChannel1', type: General.GM_CHANNEL}; + + const state = {}; + + const channels = [channel1, channel2, dmChannel1, gmChannel1]; + + test('should filter out non-public channels for public category', () => { + const filterChannelsByType = Selectors.makeFilterChannelsByType(); + + expect(filterChannelsByType(state, channels, CategoryTypes.PUBLIC)).toMatchObject([channel1]); + }); + + test('should filter out non-private channels for private category', () => { + const filterChannelsByType = Selectors.makeFilterChannelsByType(); + + expect(filterChannelsByType(state, channels, CategoryTypes.PRIVATE)).toMatchObject([channel2]); + }); + + test('should filter out non-DM/GM channels for direct messages category', () => { + const filterChannelsByType = Selectors.makeFilterChannelsByType(); + + expect(filterChannelsByType(state, channels, CategoryTypes.DIRECT_MESSAGES)).toMatchObject([dmChannel1, gmChannel1]); + }); + + test('should not filter out channels for favorites category', () => { + const filterChannelsByType = Selectors.makeFilterChannelsByType(); + + expect(filterChannelsByType(state, channels, CategoryTypes.FAVORITES)).toBe(channels); + }); +}); + +describe('makeFilterAutoclosedDMs', () => { + const currentUser = {id: 'currentUser'}; + + const baseState = { + entities: { + channels: { + currentChannelId: 'channel1', + myMembers: { + channel1: {}, + }, + }, + general: { + config: { + CloseUnusedDirectMessages: 'true', + }, + }, + posts: { + posts: {}, + postsInChannel: { + channel1: [], + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]: {value: Preferences.AUTOCLOSE_DMS_ENABLED}, + }, + }, + users: { + currentUserId: currentUser.id, + profiles: { + currentUser, + }, + }, + }, + }; + + const now = Date.now(); + const cutoff = now - (7 * 24 * 60 * 60 * 1000); + + function isChannelVisiblePrecondition(state, channel) { + return isGroupOrDirectChannelVisible( + channel, + getMyChannelMemberships(state), + getConfig(state), + getMyPreferences(state), + getCurrentUserId(state), + state.entities.users.profiles, + getLastPostPerChannel(state), + getCurrentChannelId(state), + now, + ); + } + + test('should hide an inactive GM channel', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(false); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([]); + }); + + test('should show a GM channel if it was opened recently', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff + 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show a GM channel if it was viewed recently', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff + 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show a GM channel if it had an unloaded post made recently', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL, last_post_at: cutoff + 1}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show a GM channel if it had a loaded post made recently', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + posts: { + posts: { + post1: {id: 'post1', channel_id: channel1, create_at: cutoff + 1}, + }, + postsInChannel: { + channel1: [{order: ['post1'], recent: true}], + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show an inactive GM channel if autoclosing DMs is disabled for the user', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]: {value: ''}, + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show an inactive GM channel if autoclosing DMs is disabled for the server', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + CloseUnusedDirectMessages: 'false', + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]: {value: Preferences.AUTOCLOSE_DMS_ENABLED}, + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show a GM channel if it has unread messages', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.GM_CHANNEL, total_msg_count: 1}; + + const state = mergeObjects(baseState, { + entities: { + channels: { + currentChannelId: 'channel1', + myMembers: { + channel1: {msg_count: 0}, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should hide an inactive DM channel', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const otherUser = {id: 'otherUser', delete_at: 0}; + const channel1 = {id: 'channel1', name: `${currentUser.id}__${otherUser.id}`, type: General.DM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + users: { + profiles: { + otherUser, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(false); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([]); + }); + + test('should show a DM channel if it was opened recently', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const otherUser = {id: 'otherUser', delete_at: 0}; + const channel1 = {id: 'channel1', name: `${currentUser.id}__${otherUser.id}`, type: General.DM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff + 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + users: { + profiles: { + otherUser, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should show a DM channel with a deactivated user if its the current channel', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const otherUser = {id: 'otherUser', delete_at: cutoff + 2}; + const channel1 = {id: 'channel1', name: `${currentUser.id}__${otherUser.id}`, type: General.DM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + channels: { + currentChannelId: 'channel1', + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff + 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + users: { + profiles: { + otherUser, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should hide a DM channel with a deactivated user if it is not the current channel', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const otherUser = {id: 'otherUser', delete_at: cutoff + 2}; + const channel1 = {id: 'channel1', name: `${currentUser.id}__${otherUser.id}`, type: General.DM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + channels: { + currentChannelId: 'channel2', + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff + 1}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + users: { + profiles: { + otherUser, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(false); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([]); + }); + + test('should show a DM channel with a deactivated user if it is not the current channel but it has been opened since the user was deactivated', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const otherUser = {id: 'otherUser', delete_at: cutoff + 2}; + const channel1 = {id: 'channel1', name: `${currentUser.id}__${otherUser.id}`, type: General.DM_CHANNEL}; + + const state = mergeObjects(baseState, { + entities: { + channels: { + currentChannelId: 'channel1', + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel1.id)]: {value: `${cutoff + 3}`}, + [getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel1.id)]: {value: `${cutoff - 1}`}, + }, + }, + users: { + profiles: { + otherUser, + }, + }, + }, + }); + + expect(isChannelVisiblePrecondition(state, channel1)).toBe(true); + + expect(filterAutoclosedDMs(state, [channel1], CategoryTypes.DIRECT_MESSAGES)).toEqual([channel1]); + }); + + test('should return the original array when no items are removed', () => { + const filterAutoclosedDMs = Selectors.makeFilterAutoclosedDMs(() => cutoff); + + const channel1 = {id: 'channel1', type: General.PUBLIC_CHANNEL}; + + const state = baseState; + + const channels = [channel1]; + + expect(filterAutoclosedDMs(state, channels, CategoryTypes.DIRECT_MESSAGES)).toBe(channels); + }); +}); + +describe('makeFilterManuallyClosedDMs', () => { + const currentUser = {id: 'currentUser'}; + const otherUser1 = {id: 'otherUser1'}; + const otherUser2 = {id: 'otherUser2'}; + const otherUser3 = {id: 'otherUser3'}; + + test('should filter DMs based on preferences', () => { + const filterManuallyClosedDMs = Selectors.makeFilterManuallyClosedDMs(); + + const dmChannel1 = {id: 'dmChannel1', type: General.DM_CHANNEL, name: `${currentUser.id}__${otherUser1.id}`}; + const dmChannel2 = {id: 'dmChannel2', type: General.DM_CHANNEL, name: `${currentUser.id}__${otherUser2.id}`}; + + const state = { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser1.id)]: {value: 'false'}, + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser2.id)]: {value: 'true'}, + }, + }, + users: { + currentUserId: currentUser.id, + }, + }, + }; + + expect(filterManuallyClosedDMs(state, [dmChannel1, dmChannel2])).toMatchObject([dmChannel2]); + }); + + test('should filter DMs based on preferences', () => { + const filterManuallyClosedDMs = Selectors.makeFilterManuallyClosedDMs(); + + const gmChannel1 = {id: 'gmChannel1', type: General.GM_CHANNEL}; + const gmChannel2 = {id: 'gmChannel2', type: General.GM_CHANNEL}; + + const state = { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, gmChannel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, gmChannel2.id)]: {value: 'false'}, + }, + }, + users: { + currentUserId: currentUser.id, + }, + }, + }; + + expect(filterManuallyClosedDMs(state, [gmChannel1, gmChannel2])).toMatchObject([gmChannel1]); + }); + + test('should not filter other channels', () => { + const filterManuallyClosedDMs = Selectors.makeFilterManuallyClosedDMs(); + + const channel1 = {id: 'channel1', type: General.OPEN_CHANNEL}; + const channel2 = {id: 'channel2', type: General.PRIVATE_CHANNEL}; + + const state = { + entities: { + preferences: { + myPreferences: {}, + }, + users: { + currentUserId: currentUser.id, + }, + }, + }; + + const channels = [channel1, channel2]; + + expect(filterManuallyClosedDMs(state, channels)).toBe(channels); + }); +}); + +describe('makeSortChannelsByName', () => { + const currentUser = {id: 'currentUser', locale: 'en'}; + + const baseState = { + entities: { + users: { + currentUserId: currentUser.id, + profiles: { + currentUser, + }, + }, + }, + }; + + test('should sort channels by display name', () => { + const sortChannelsByName = Selectors.makeSortChannelsByName(); + + const channel1 = {id: 'channel1', display_name: 'Carrot'}; + const channel2 = {id: 'channel2', display_name: 'Apple'}; + const channel3 = {id: 'channel3', display_name: 'Banana'}; + const channels = [channel1, channel2, channel3]; + + expect(sortChannelsByName(baseState, channels)).toEqual([channel2, channel3, channel1]); + }); + + test('should sort channels by display name with numbers', () => { + const sortChannelsByName = Selectors.makeSortChannelsByName(); + + const channel1 = {id: 'channel1', display_name: 'Channel 10'}; + const channel2 = {id: 'channel2', display_name: 'Channel 1'}; + const channel3 = {id: 'channel3', display_name: 'Channel 11'}; + const channel4 = {id: 'channel4', display_name: 'Channel 1a'}; + const channels = [channel1, channel2, channel3, channel4]; + + expect(sortChannelsByName(baseState, channels)).toEqual([channel2, channel4, channel1, channel3]); + }); +}); + +describe('makeSortChannelsByNameWithDMs', () => { + const currentUser = {id: 'currentUser', username: 'currentUser', first_name: 'Current', last_name: 'User', locale: 'en'}; + const otherUser1 = {id: 'otherUser1', username: 'otherUser1', first_name: 'Other', last_name: 'User', locale: 'en'}; + const otherUser2 = {id: 'otherUser2', username: 'otherUser2', first_name: 'Another', last_name: 'User', locale: 'en'}; + + const channel1 = {id: 'channel1', type: General.OPEN_CHANNEL, display_name: 'Zebra'}; + const channel2 = {id: 'channel2', type: General.PRIVATE_CHANNEL, display_name: 'Aardvark'}; + const channel3 = {id: 'channel3', type: General.OPEN_CHANNEL, display_name: 'Bear'}; + const dmChannel1 = {id: 'dmChannel1', type: General.DM_CHANNEL, display_name: '', name: `${currentUser.id}__${otherUser1.id}`}; + const dmChannel2 = {id: 'dmChannel2', type: General.DM_CHANNEL, display_name: '', name: `${otherUser2.id}__${currentUser.id}`}; + const gmChannel1 = {id: 'gmChannel1', type: General.GM_CHANNEL, display_name: `${currentUser.username}, ${otherUser1.username}, ${otherUser2.username}`, name: 'gmChannel1'}; + + const baseState = { + entities: { + general: { + config: {}, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT)]: {value: Preferences.DISPLAY_PREFER_FULL_NAME}, + }, + }, + users: { + currentUserId: currentUser.id, + profiles: { + currentUser, + otherUser1, + otherUser2, + }, + }, + }, + }; + + test('should sort regular channels by display name', () => { + const sortChannelsByNameWithDMs = Selectors.makeSortChannelsByNameWithDMs(); + + expect(sortChannelsByNameWithDMs(baseState, [ + channel1, + channel2, + channel3, + ])).toMatchObject([ + channel2, // Aardvark + channel3, // Bear + channel1, // Zebra + ]); + }); + + test('should sort DM channels by the display name of the other user', () => { + const sortChannelsByNameWithDMs = Selectors.makeSortChannelsByNameWithDMs(); + + expect(sortChannelsByNameWithDMs(baseState, [ + channel1, + channel2, + channel3, + dmChannel1, + dmChannel2, + ])).toMatchObject([ + channel2, // Aardvark + dmChannel2, // Another User + channel3, // Bear + dmChannel1, // Other User + channel1, // Zebra + ]); + }); + + test('should sort GM channels by the display name of the other users', () => { + const sortChannelsByNameWithDMs = Selectors.makeSortChannelsByNameWithDMs(); + + expect(sortChannelsByNameWithDMs(baseState, [ + channel1, + channel2, + channel3, + gmChannel1, + ])).toMatchObject([ + channel2, // Aardvark + gmChannel1, // Another User, Other User + channel3, // Bear + channel1, // Zebra + ]); + + const state = { + ...baseState, + entities: { + ...baseState.entities, + users: { + ...baseState.entities.users, + currentUserId: otherUser2.id, + }, + }, + }; + + expect(sortChannelsByNameWithDMs(state, [ + channel1, + channel2, + channel3, + gmChannel1, + ])).toMatchObject([ + channel2, // Aardvark + channel3, // Bear + gmChannel1, // Current User, Other User + channel1, // Zebra + ]); + }); +}); + +describe('makeGetChannelsForCategory', () => { + const currentUser = {id: 'currentUser', username: 'currentUser', first_name: 'Current', last_name: 'User', locale: 'en'}; + const otherUser1 = {id: 'otherUser1', username: 'otherUser1', first_name: 'Other', last_name: 'User', locale: 'en'}; + const otherUser2 = {id: 'otherUser2', username: 'otherUser2', first_name: 'Another', last_name: 'User', locale: 'en'}; + + const channel1 = {id: 'channel1', type: General.OPEN_CHANNEL, team_id: 'team1', display_name: 'Zebra', delete_at: 0}; + const channel2 = {id: 'channel2', type: General.PRIVATE_CHANNEL, team_id: 'team1', display_name: 'Aardvark', delete_at: 0}; + const channel3 = {id: 'channel3', type: General.OPEN_CHANNEL, team_id: 'team1', display_name: 'Bear', delete_at: 0}; + const dmChannel1 = {id: 'dmChannel1', type: General.DM_CHANNEL, team_id: '', display_name: '', name: `${currentUser.id}__${otherUser1.id}`, delete_at: 0}; + const dmChannel2 = {id: 'dmChannel2', type: General.DM_CHANNEL, team_id: '', display_name: '', name: `${otherUser2.id}__${currentUser.id}`, delete_at: 0}; + const gmChannel1 = {id: 'gmChannel1', type: General.GM_CHANNEL, team_id: '', display_name: `${currentUser.username}, ${otherUser1.username}, ${otherUser2.username}`, name: 'gmChannel1', delete_at: 0}; + + const favoritesCategory = {id: 'favoritesCategory', team_id: 'team1', display_name: CategoryTypes.FAVORITES, type: CategoryTypes.FAVORITES}; + const publicCategory = {id: 'publicCategory', team_id: 'team1', display_name: 'Public Channels', type: CategoryTypes.PUBLIC}; + const privateCategory = {id: 'privateCategory', team_id: 'team1', display_name: 'Private Channels', type: CategoryTypes.PRIVATE}; + const directMessagesCategory = {id: 'directMessagesCategory', team_id: 'team1', display_name: 'Direct Messages', type: CategoryTypes.DIRECT_MESSAGES}; + + const state = { + entities: { + channelCategories: { + byId: { + favoritesCategory, + publicCategory, + privateCategory, + directMessagesCategory, + }, + }, + channels: { + channels: { + channel1, + channel2, + channel3, + dmChannel1, + dmChannel2, + gmChannel1, + }, + myMembers: { + [channel1.id]: {}, + [channel2.id]: {}, + [channel3.id]: {}, + [dmChannel1.id]: {}, + [dmChannel2.id]: {}, + [gmChannel1.id]: {}, + }, + }, + general: { + config: {}, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT)]: {value: Preferences.DISPLAY_PREFER_FULL_NAME}, + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser2.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, dmChannel2.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, gmChannel1.id)]: {value: 'true'}, + }, + }, + users: { + currentUserId: currentUser.id, + profiles: { + currentUser, + otherUser1, + otherUser2, + }, + }, + }, + }; + + test('should return sorted and filtered channels for favorites category', () => { + const getChannelsForCategory = Selectors.makeGetChannelsForCategory(); + + expect(getChannelsForCategory(state, favoritesCategory)).toMatchObject([dmChannel2, channel1]); + }); + + test('should return sorted and filtered channels for public category', () => { + const getChannelsForCategory = Selectors.makeGetChannelsForCategory(); + + expect(getChannelsForCategory(state, publicCategory)).toMatchObject([channel3]); + }); + + test('should return sorted and filtered channels for private category', () => { + const getChannelsForCategory = Selectors.makeGetChannelsForCategory(); + + expect(getChannelsForCategory(state, privateCategory)).toMatchObject([channel2]); + }); + + test('should return sorted and filtered channels for direct messages category', () => { + const getChannelsForCategory = Selectors.makeGetChannelsForCategory(); + + expect(getChannelsForCategory(state, directMessagesCategory)).toMatchObject([gmChannel1, dmChannel1]); + }); +}); + +describe('makeGetChannelsByCategory', () => { + const currentUser = {id: 'currentUser', username: 'currentUser', first_name: 'Current', last_name: 'User', locale: 'en'}; + const otherUser1 = {id: 'otherUser1', username: 'otherUser1', first_name: 'Other', last_name: 'User', locale: 'en'}; + const otherUser2 = {id: 'otherUser2', username: 'otherUser2', first_name: 'Another', last_name: 'User', locale: 'en'}; + + const channel1 = {id: 'channel1', type: General.OPEN_CHANNEL, team_id: 'team1', display_name: 'Zebra', delete_at: 0}; + const channel2 = {id: 'channel2', type: General.PRIVATE_CHANNEL, team_id: 'team1', display_name: 'Aardvark', delete_at: 0}; + const channel3 = {id: 'channel3', type: General.OPEN_CHANNEL, team_id: 'team1', display_name: 'Bear', delete_at: 0}; + const dmChannel1 = {id: 'dmChannel1', type: General.DM_CHANNEL, team_id: '', display_name: '', name: `${currentUser.id}__${otherUser1.id}`, delete_at: 0}; + const dmChannel2 = {id: 'dmChannel2', type: General.DM_CHANNEL, team_id: '', display_name: '', name: `${otherUser2.id}__${currentUser.id}`, delete_at: 0}; + const gmChannel1 = {id: 'gmChannel1', type: General.GM_CHANNEL, team_id: '', display_name: `${currentUser.username}, ${otherUser1.username}, ${otherUser2.username}`, name: 'gmChannel1', delete_at: 0}; + + const favoritesCategory = {id: 'favoritesCategory', team_id: 'team1', display_name: CategoryTypes.FAVORITES, type: CategoryTypes.FAVORITES}; + const publicCategory = {id: 'publicCategory', team_id: 'team1', display_name: 'Public Channels', type: CategoryTypes.PUBLIC}; + const privateCategory = {id: 'privateCategory', team_id: 'team1', display_name: 'Private Channels', type: CategoryTypes.PRIVATE}; + const directMessagesCategory = {id: 'directMessagesCategory', team_id: 'team1', display_name: 'Direct Messages', type: CategoryTypes.DIRECT_MESSAGES}; + + const baseState = { + entities: { + channelCategories: { + byId: { + favoritesCategory, + publicCategory, + privateCategory, + directMessagesCategory, + }, + orderByTeam: { + team1: [ + favoritesCategory.id, + publicCategory.id, + privateCategory.id, + directMessagesCategory.id, + ], + }, + }, + channels: { + channels: { + channel1, + channel2, + channel3, + dmChannel1, + dmChannel2, + gmChannel1, + }, + myMembers: { + [channel1.id]: {}, + [channel2.id]: {}, + [channel3.id]: {}, + [dmChannel1.id]: {}, + [dmChannel2.id]: {}, + [gmChannel1.id]: {}, + }, + }, + general: { + config: {}, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT)]: {value: Preferences.DISPLAY_PREFER_FULL_NAME}, + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser2.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, channel1.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, dmChannel2.id)]: {value: 'true'}, + [getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, gmChannel1.id)]: {value: 'true'}, + }, + }, + users: { + currentUserId: currentUser.id, + profiles: { + currentUser, + otherUser1, + otherUser2, + }, + }, + }, + }; + + test('should return channels for all categories', () => { + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + expect(getChannelsByCategory(baseState, 'team1')).toEqual({ + favoritesCategory: [dmChannel2, channel1], + publicCategory: [channel3], + privateCategory: [channel2], + directMessagesCategory: [gmChannel1, dmChannel1], + }); + }); + + describe('memoization', () => { + test('should return the same object when called with the same state', () => { + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + expect(getChannelsByCategory(baseState, 'team1')).toBe(getChannelsByCategory(baseState, 'team1')); + }); + + test('should return the same object when unrelated state changes', () => { + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + const state = mergeObjects(baseState, { + views: { + something: 7, + }, + }); + + const previousResult = getChannelsByCategory(baseState, 'team1'); + const result = getChannelsByCategory(state, 'team1'); + + expect(result).toBe(previousResult); + }); + + test('should return a new object when user profiles change', () => { + // This behaviour isn't ideal, but it's better than the previous version which returns a new object + // whenever anything user-related changes + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + const state = mergeObjects(baseState, { + entities: { + users: { + profiles: { + newUser: {id: 'newUser'}, + }, + }, + }, + }); + + const previousResult = getChannelsByCategory(baseState, 'team1'); + const result = getChannelsByCategory(state, 'team1'); + + expect(result).not.toBe(previousResult); + expect(result).toEqual(previousResult); + + // Categories not containing DMs/GMs should still remain the same + expect(result.favoritesCategory).not.toBe(previousResult.favoritesCategory); + expect(result.favoritesCategory).toEqual(previousResult.favoritesCategory); + expect(result.publicCategory).toBe(previousResult.publicCategory); + expect(result.privateCategory).toBe(previousResult.privateCategory); + expect(result.directMessagesCategory).not.toBe(previousResult.directMessagesCategory); + expect(result.directMessagesCategory).toEqual(previousResult.directMessagesCategory); + }); + + test('should return the same object when other user state changes', () => { + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + const state = mergeObjects(baseState, { + entities: { + users: { + statuses: { + otherUser1: 'offline', + }, + }, + }, + }); + + const previousResult = getChannelsByCategory(baseState, 'team1'); + const result = getChannelsByCategory(state, 'team1'); + + expect(result).toBe(previousResult); + }); + + test('should return a new object when preferences change', () => { + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey('abc', '123')]: {value: 'true'}, + }, + }, + }, + }); + + const previousResult = getChannelsByCategory(baseState, 'team1'); + const result = getChannelsByCategory(state, 'team1'); + + expect(result).not.toBe(previousResult); + expect(result).toEqual(previousResult); + }); + + test('should return a new object when a DM is closed', () => { + const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); + + const state = mergeObjects(baseState, { + entities: { + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUser1.id)]: {value: 'false'}, + }, + }, + }, + }); + + const previousResult = getChannelsByCategory(baseState, 'team1'); + const result = getChannelsByCategory(state, 'team1'); + + expect(result).not.toBe(previousResult); + expect(result.favoritesCategory).toEqual(previousResult.favoritesCategory); + expect(result.publicCategory).toEqual(previousResult.publicCategory); + expect(result.privateCategory).toEqual(previousResult.privateCategory); + expect(result.directMessagesCategory).not.toEqual(previousResult.directMessagesCategory); + }); + }); +}); diff --git a/app/mm-redux/selectors/entities/channel_categories.ts b/app/mm-redux/selectors/entities/channel_categories.ts new file mode 100644 index 000000000..9f5b2552a --- /dev/null +++ b/app/mm-redux/selectors/entities/channel_categories.ts @@ -0,0 +1,375 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {createSelector} from 'reselect'; +import shallowEquals from 'shallow-equals'; + +import {General, Preferences} from '../../constants'; +import {CategoryTypes} from '../../constants/channel_categories'; + +import {getCurrentChannelId, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserLocale} from '@mm-redux/selectors/entities/i18n'; +import {getLastPostPerChannel} from '@mm-redux/selectors/entities/posts'; +import {getMyPreferences, getTeammateNameDisplaySetting, shouldAutocloseDMs} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; + +import {Channel, ChannelMembership} from '@mm-redux/types/channels'; +import {ChannelCategory} from '@mm-redux/types/channel_categories'; +import {GlobalState} from '@mm-redux/types/store'; +import {UserProfile} from '@mm-redux/types/users'; +import {IDMappedObjects, RelationOneToOne} from '@mm-redux/types/utilities'; + +import {getUserIdFromChannelName, isFavoriteChannel, isUnreadChannel} from '@mm-redux/utils/channel_utils'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; + +export function getCategoryIdsForTeam(state: GlobalState, teamId: string): string[] | undefined { + return state.entities.channelCategories.orderByTeam[teamId]; +} + +export function makeGetCategoriesForTeam(): (state: GlobalState, teamId: string) => ChannelCategory[] { + return createSelector( + getCategoryIdsForTeam, + (state: GlobalState) => state.entities.channelCategories.byId, + (categoryIds, categoriesById) => { + if (!categoryIds) { + return []; + } + + return categoryIds.map((id) => categoriesById[id]); + } + ); +} + +export function makeGetUnsortedUnfilteredChannels(): (state: GlobalState, teamId: string) => Channel[] { + return createSelector( + (state: GlobalState) => state.entities.channels.channels, + getMyChannelMemberships, + (state: GlobalState, teamId: string) => teamId, + (allChannels: IDMappedObjects, myMembers: RelationOneToOne, teamId: string) => { + return Object.values(allChannels). + filter((channel) => channel.delete_at === 0). + filter((channel) => channel.team_id === teamId || channel.team_id === ''). + filter((channel) => myMembers.hasOwnProperty(channel.id)); + } + ); +} + +export function makeFilterChannelsByFavorites(): (state: GlobalState, channels: Channel[], categoryType: string) => Channel[] { + return createSelector( + (state: GlobalState, channels: Channel[]) => channels, + (state: GlobalState, channels: Channel[], categoryType: string) => categoryType, + getMyPreferences, + (channels, categoryType, myPreferences) => { + const filtered = channels.filter((channel) => { + if (categoryType === CategoryTypes.FAVORITES) { + return isFavoriteChannel(myPreferences, channel.id); + } + + return !isFavoriteChannel(myPreferences, channel.id); + }); + + return filtered.length === channels.length ? channels : filtered; + } + ); +} + +export function makeFilterChannelsByType(): (state: GlobalState, channels: Channel[], categoryType: string) => Channel[] { + // This doesn't need to be a selector, but make it as one to keep it consistent + return createSelector( + (state: GlobalState, channels: Channel[]) => channels, + (state: GlobalState, channels: Channel[], categoryType: string) => categoryType, + (channels, categoryType) => { + const filtered = channels.filter((channel) => { + if (categoryType === CategoryTypes.PUBLIC) { + return channel.type === General.OPEN_CHANNEL; + } else if (categoryType === CategoryTypes.PRIVATE) { + return channel.type === General.PRIVATE_CHANNEL; + } else if (categoryType === CategoryTypes.DIRECT_MESSAGES) { + return channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL; + } + + return true; + }); + + return filtered.length === channels.length ? channels : filtered; + } + ); +} + +function getDefaultAutocloseCutoff() { + return Date.now() - 7 * 24 * 60 * 60 * 1000; +} + +export function makeFilterAutoclosedDMs(getAutocloseCutoff = getDefaultAutocloseCutoff): (state: GlobalState, channels: Channel[], categoryType: string) => Channel[] { + return createSelector( + (state: GlobalState, channels: Channel[]) => channels, + (state: GlobalState, channels: Channel[], categoryType: string) => categoryType, + getMyPreferences, + shouldAutocloseDMs, + getCurrentChannelId, + (state: GlobalState) => state.entities.users.profiles, + getCurrentUserId, + getMyChannelMemberships, + getLastPostPerChannel, + (channels, categoryType, myPreferences, autocloseDMs, currentChannelId, profiles, currentUserId, myChannelMembers, lastPosts) => { + if (categoryType !== CategoryTypes.DIRECT_MESSAGES) { + // Only autoclose DMs that haven't been assigned to a category + return channels; + } + + // Ideally, this would come from a selector, but that would cause the filter to recompute too often + const cutoff = getAutocloseCutoff(); + + const filtered = channels.filter((channel) => { + if (channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL) { + return true; + } + + // Unread channels will never be hidden + if (isUnreadChannel(myChannelMembers, channel)) { + return true; + } + + // viewTime is the time the channel was last viewed by the user + const viewTimePref = myPreferences[getPreferenceKey(Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel.id)]; + const viewTime = parseInt(viewTimePref ? viewTimePref.value! : '0', 10); + + // Recently viewed channels will never be hidden. Note that viewTime is not set correctly at the time of writing. + if (viewTime > cutoff) { + return true; + } + + // openTime is the time the channel was last opened (like from the More DMs list) after having been closed + const openTimePref = myPreferences[getPreferenceKey(Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel.id)]; + const openTime = parseInt(openTimePref ? openTimePref.value! : '0', 10); + + // DMs with deactivated users will be visible if you're currently viewing them and they were opened + // since the user was deactivated + if (channel.type === General.DM_CHANNEL && channel.id !== currentChannelId) { + const teammateId = getUserIdFromChannelName(currentUserId, channel.name); + const teammate = profiles[teammateId]; + + if (!teammate || teammate.delete_at > openTime) { + return false; + } + } + + // Skip the rest of the checks if autoclosing inactive DMs is disabled + if (!autocloseDMs) { + return true; + } + + // Keep the channel open if it had a recent post. If we have posts loaded for the channel, use the create_at + // of the last post in the channel since channel.last_post_at isn't kept up to date on the client. If we don't + // have posts loaded, then fall back to the last_post_at. + const lastPost = lastPosts[channel.id]; + + if (lastPost && lastPost.create_at > cutoff) { + return true; + } + + if (openTime > cutoff) { + return true; + } + + if (channel.last_post_at && channel.last_post_at > cutoff) { + return true; + } + + return false; + }); + + return filtered.length === channels.length ? channels : filtered; + } + ); +} + +export function makeFilterManuallyClosedDMs(): (state: GlobalState, channels: Channel[]) => Channel[] { + return createSelector( + (state: GlobalState, channels: Channel[]) => channels, + getMyPreferences, + getCurrentUserId, + (channels, myPreferences, currentUserId) => { + const filtered = channels.filter((channel) => { + let preference; + + if (channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL) { + return true; + } + + if (channel.type === General.DM_CHANNEL) { + const teammateId = getUserIdFromChannelName(currentUserId, channel.name); + + preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammateId)]; + } else { + preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel.id)]; + } + + return preference && preference.value !== 'false'; + }); + + // Only return a new array if anything was removed + return filtered.length === channels.length ? channels : filtered; + } + ); +} + +export function makeSortChannelsByName(): (state: GlobalState, channels: Channel[]) => Channel[] { + return createSelector( + (state: GlobalState, channels: Channel[]) => channels, + getCurrentUserLocale, + (channels: Channel[], locale: string) => { + const sorted = [...channels]; + sorted.sort((a, b) => a.display_name.localeCompare(b.display_name, locale, {numeric: true})); + return sorted; + } + ); +} + +export function makeSortChannelsByNameWithDMs(): (state: GlobalState, channels: Channel[]) => Channel[] { + return createSelector( + (state: GlobalState, channels: Channel[]) => channels, + getCurrentUserId, + (state: GlobalState) => state.entities.users.profiles, + getTeammateNameDisplaySetting, + getCurrentUserLocale, + (channels: Channel[], currentUserId: string, profiles: IDMappedObjects, teammateNameDisplay: string, locale: string) => { + const cachedNames: RelationOneToOne = {}; + + const getDisplayName = (channel: Channel): string => { + if (cachedNames[channel.id]) { + return cachedNames[channel.id]; + } + + let displayName; + + // TODO it might be easier to do this by using channel members to find the users + if (channel.type === General.DM_CHANNEL) { + const teammateId = getUserIdFromChannelName(currentUserId, channel.name); + const teammate = profiles[teammateId]; + + displayName = displayUsername(teammate, teammateNameDisplay, false); + } else if (channel.type === General.GM_CHANNEL) { + const usernames = channel.display_name.split(', '); + + const userDisplayNames = []; + for (const username of usernames) { + const user = Object.values(profiles).find((profile) => profile.username === username); + + if (!user) { + continue; + } + + if (user.id === currentUserId) { + continue; + } + + userDisplayNames.push(displayUsername(user, teammateNameDisplay, false)); + } + + displayName = userDisplayNames.sort((a, b) => a.localeCompare(b, locale, {numeric: true})).join(', '); + } else { + displayName = channel.display_name; + } + + cachedNames[channel.id] = displayName; + + return displayName; + }; + + const sorted = [...channels]; + sorted.sort((a, b) => getDisplayName(a).localeCompare(getDisplayName(b), locale, {numeric: true})); + return sorted; + } + ); +} + +export function makeGetChannelsForCategory() { + const getUnsortedUnfilteredChannels = makeGetUnsortedUnfilteredChannels(); + const filterAndSortChannelsForCategory = makeFilterAndSortChannelsForCategory(); + + return (state: GlobalState, category: ChannelCategory) => { + const channels = getUnsortedUnfilteredChannels(state, category.team_id); + + return filterAndSortChannelsForCategory(state, channels, category); + }; +} + +export function makeFilterAndSortChannelsForCategory() { + const filterChannelsByFavorites = makeFilterChannelsByFavorites(); + const filterChannelsByType = makeFilterChannelsByType(); + + const filterAutoclosedDMs = makeFilterAutoclosedDMs(); + const filterManuallyClosedDMs = makeFilterManuallyClosedDMs(); + + const sortChannelsByName = makeSortChannelsByName(); + const sortChannelsByNameWithDMs = makeSortChannelsByNameWithDMs(); + + return (state: GlobalState, originalChannels: Channel[], category: ChannelCategory) => { + let channels = originalChannels; + + channels = filterChannelsByFavorites(state, channels, category.type); + channels = filterChannelsByType(state, channels, category.type); + + channels = filterAutoclosedDMs(state, channels, category.type); + channels = filterManuallyClosedDMs(state, channels); + + if (channels.some((channel) => channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL)) { + channels = sortChannelsByNameWithDMs(state, channels); + } else { + channels = sortChannelsByName(state, channels); + } + + return channels; + }; +} + +export function makeGetChannelsByCategory() { + const getCategoriesForTeam = makeGetCategoriesForTeam(); + const getUnsortedUnfilteredChannels = makeGetUnsortedUnfilteredChannels(); + + // Memoize filterAndSortChannels by category. As long as the categories don't change, we can keep using the same + // selector for each category. + let filterAndSortChannels: RelationOneToOne>; + + let lastCategoryIds: ReturnType = []; + let lastChannelsByCategory: RelationOneToOne = {}; + + return (state: GlobalState, teamId: string) => { + const categoryIds = getCategoryIdsForTeam(state, teamId); + + // Create an instance of filterAndSortChannels for each category. As long as we don't add or remove new categories, + // we can reuse these selectors to memoize the results of each category. This will also create new selectors when + // categories are reordered, but that should be rare enough that it won't meaningfully affect performance. + if (categoryIds !== lastCategoryIds) { + lastCategoryIds = categoryIds; + lastChannelsByCategory = {}; + + filterAndSortChannels = {}; + + if (categoryIds) { + for (const categoryId of categoryIds) { + filterAndSortChannels[categoryId] = makeFilterAndSortChannelsForCategory(); + } + } + } + + const categories = getCategoriesForTeam(state, teamId); + const channels = getUnsortedUnfilteredChannels(state, teamId); + + const channelsByCategory: RelationOneToOne = {}; + + for (const category of categories) { + channelsByCategory[category.id] = filterAndSortChannels[category.id](state, channels, category); + } + + // Do a shallow equality check of channelsByCategory to avoid returning a new object containing the same data + if (shallowEquals(channelsByCategory, lastChannelsByCategory)) { + return lastChannelsByCategory; + } + + lastChannelsByCategory = channelsByCategory; + return channelsByCategory; + }; +} diff --git a/app/mm-redux/selectors/entities/channels.test.js b/app/mm-redux/selectors/entities/channels.test.js new file mode 100644 index 000000000..adaa2ef81 --- /dev/null +++ b/app/mm-redux/selectors/entities/channels.test.js @@ -0,0 +1,3865 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import TestHelper from 'test/test_helper'; +import {sortChannelsByDisplayName, getDirectChannelName} from '@mm-redux/utils/channel_utils'; +import * as Selectors from '@mm-redux/selectors/entities/channels'; +import * as TeamSelectors from '@mm-redux/selectors/entities/teams'; +import * as PreferencesSelectors from '@mm-redux/selectors/entities/preferences'; +import {General, Preferences, Permissions} from '../../constants'; + +const sortUsernames = (a, b) => a.localeCompare(b, General.DEFAULT_LOCALE, {numeric: true}); + +describe('Selectors.Channels.getChannelsInCurrentTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + it('should return channels in current team', () => { + const user = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + }; + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team2.id); + const channel3 = TestHelper.fakeChannelWithId(team1.id); + const channel4 = TestHelper.fakeChannelWithId(''); + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel3.id], + [team2.id]: [channel2.id], + '': [channel4.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + }, + }, + }); + + const channelsInCurrentTeam = [channel1, channel3].sort(sortChannelsByDisplayName.bind(null, [])); + assert.deepEqual(Selectors.getChannelsInCurrentTeam(testState), channelsInCurrentTeam); + }); + + it('should order by user locale', () => { + const userDe = { + ...TestHelper.fakeUserWithId(), + locale: 'de', + }; + const userSv = { + ...TestHelper.fakeUserWithId(), + locale: 'sv', + }; + + const profilesDe = { + [userDe.id]: userDe, + }; + const profilesSv = { + [userSv.id]: userSv, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'z', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'ä', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + }; + + const testStateDe = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userDe.id, + profiles: profilesDe, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + }, + }, + }); + + const testStateSv = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userSv.id, + profiles: profilesSv, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + }, + }, + }); + + const channelsInCurrentTeamDe = [channel1, channel2].sort(sortChannelsByDisplayName.bind(null, userDe.locale)); + const channelsInCurrentTeamSv = [channel1, channel2].sort(sortChannelsByDisplayName.bind(null, userSv.locale)); + + assert.deepEqual(Selectors.getChannelsInCurrentTeam(testStateDe), channelsInCurrentTeamDe); + assert.deepEqual(Selectors.getChannelsInCurrentTeam(testStateSv), channelsInCurrentTeamSv); + }); +}); + +describe('Selectors.Channels.getKnownUsers', () => { + const channel1 = TestHelper.fakeChannelWithId(''); + const channel2 = TestHelper.fakeChannelWithId(''); + + const me = TestHelper.fakeUserWithId(); + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const membersInChannel = { + [channel1.id]: { + [user.id]: {channel_id: channel1.id, user_id: user.id}, + [user2.id]: {channel_id: channel1.id, user_id: user2.id}, + }, + [channel2.id]: { + [user3.id]: {channel_id: channel2.id, user_id: user3.id}, + }, + }; + + it('should return all members of all my channels', () => { + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: me.id, + }, + channels: { + membersInChannel, + }, + }, + }); + + assert.deepEqual(Selectors.getKnownUsers(testState), new Set([me.id, user.id, user2.id, user3.id])); + }); + + it('should return only me if I have no channels', () => { + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: me.id, + }, + channels: { + membersInChannel: {}, + }, + }, + }); + + assert.deepEqual(Selectors.getKnownUsers(testState), new Set([me.id])); + }); +}); + +describe('Selectors.Channels.getMyChannels', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + [user2.id]: user2, + [user3.id]: user3, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + display_name: 'Channel Name', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(''), + display_name: 'Channel Name', + type: General.DM_CHANNEL, + name: getDirectChannelName(user.id, user2.id), + }; + const channel5 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: [user.username, user2.username, user3.username].join(', '), + type: General.GM_CHANNEL, + name: '', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel3.id], + [team2.id]: [channel2.id], + '': [channel4.id, channel5.id], + }; + + const myMembers = { + [channel1.id]: {}, + [channel3.id]: {}, + [channel4.id]: {}, + [channel5.id]: {}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + statuses: {}, + profilesInChannel: { + [channel4.id]: new Set([user.id, user2.id]), + [channel5.id]: new Set([user.id, user2.id, user3.id]), + }, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('get my channels in current team and DMs', () => { + const channelsInCurrentTeam = [channel1, channel3].sort(sortChannelsByDisplayName.bind(null, [])); + assert.deepEqual(Selectors.getMyChannels(testState), [ + ...channelsInCurrentTeam, + {...channel4, display_name: user2.username, status: 'offline', teammate_id: user2.id}, + {...channel5, display_name: [user2.username, user3.username].sort(sortUsernames).join(', ')}, + ]); + }); +}); + +describe('Selectors.Channels.getMembersInCurrentChannel', () => { + const channel1 = TestHelper.fakeChannelWithId(''); + + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const membersInChannel = { + [channel1.id]: { + [user.id]: {}, + [user2.id]: {}, + [user3.id]: {}, + }, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + currentChannelId: channel1.id, + membersInChannel, + }, + }, + }); + + it('should return members in current channel', () => { + assert.deepEqual(Selectors.getMembersInCurrentChannel(testState), membersInChannel[channel1.id]); + }); +}); + +describe('Selectors.Channels.getOtherChannels', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const user = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + type: General.PRIVATE_CHANNEL, + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(''), + display_name: 'Channel Name', + type: General.DM_CHANNEL, + }; + const channel5 = { + ...TestHelper.fakeChannelWithId(''), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + delete_at: 444, + }; + const channel6 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + [channel6.id]: channel6, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel3.id, channel5.id, channel6.id], + [team2.id]: [channel2.id], + '': [channel4.id], + }; + + const myMembers = { + [channel4.id]: {}, + [channel6.id]: {}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers, + }, + }, + }); + + it('get public channels not member of', () => { + assert.deepEqual(Selectors.getOtherChannels(testState), [channel1, channel5].sort(sortChannelsByDisplayName.bind(null, []))); + }); + + it('get public, unarchived channels not member of', () => { + assert.deepEqual(Selectors.getOtherChannels(testState, false), [channel1]); + }); +}); + +describe('Selectors.Channels.getArchivedChannels', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const user = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + delete_at: 222, + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + type: General.PRIVATE_CHANNEL, + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(''), + display_name: 'Channel Name', + type: General.DM_CHANNEL, + }; + const channel5 = { + ...TestHelper.fakeChannelWithId(''), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + delete_at: 555, + }; + const channel6 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + [channel6.id]: channel6, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel3.id, channel5.id, channel6.id], + [team2.id]: [channel2.id], + '': [channel4.id], + }; + + const myMembers = { + [channel4.id]: {}, + [channel5.id]: {}, + [channel6.id]: {}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers, + }, + }, + }); + + it('get archived channels that user is member of', () => { + assert.deepEqual(Selectors.getArchivedChannels(testState), [channel5]); + }); +}); + +describe('Selectors.Channels.getChannel', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + [user2.id]: user2, + [user3.id]: user3, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + type: General.DM_CHANNEL, + name: getDirectChannelName(user.id, user2.id), + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team2.id), + type: General.GM_CHANNEL, + display_name: [user.username, user2.username, user3.username].join(', '), + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + statuses: {}, + profilesInChannel: { + [channel2.id]: new Set([user.id, user2.id]), + [channel3.id]: new Set([user.id, user2.id, user3.id]), + }, + }, + channels: { + channels, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('get channel', () => { + assert.deepEqual(Selectors.getChannel(testState, channel1.id), channel1); + }); + it('get channel as Direct Channel', () => { + assert.deepEqual(Selectors.getChannel(testState, channel2.id), {...channel2, display_name: user2.username, status: 'offline', teammate_id: user2.id}); + }); + it('get channel as Group Message Channel', () => { + assert.deepEqual(Selectors.getChannel(testState, channel3.id), {...channel3, display_name: [user2.username, user3.username].sort(sortUsernames).join(', ')}); + }); +}); + +describe('Selectors.Channels.getChannelByName', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: 'ch1', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + name: 'ch2', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: 'ch3', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + channels, + }, + }, + }); + it('get first channel that matches by name', () => { + assert.deepEqual(Selectors.getChannelByName(testState, channel3.name), channel3); + }); + + it('return null if no channel matches by name', () => { + assert.deepEqual(Selectors.getChannelByName(testState, 'noChannel'), null); + }); +}); + +describe('Selectors.Channels.getChannelsNameMapInCurrentTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: 'Ch1', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + name: 'Ch2', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team2.id), + name: 'Ch3', + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: 'Ch4', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel4.id], + [team2.id]: [channel2.id, channel3.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + }, + }, + }); + + it('get channel map for current team', () => { + const channelMap = { + [channel1.name]: channel1, + [channel4.name]: channel4, + }; + assert.deepEqual(Selectors.getChannelsNameMapInCurrentTeam(testState), channelMap); + }); +}); + +describe('Selectors.Channels.getChannelsNameMapInTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: 'Ch1', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + name: 'Ch2', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team2.id), + name: 'Ch3', + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: 'Ch4', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel4.id], + [team2.id]: [channel2.id, channel3.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + channels, + channelsInTeam, + }, + }, + }); + it('get channel map for team', () => { + const channelMap = { + [channel1.name]: channel1, + [channel4.name]: channel4, + }; + assert.deepEqual(Selectors.getChannelsNameMapInTeam(testState, team1.id), channelMap); + }); + it('get empty map for non-existing team', () => { + assert.deepEqual(Selectors.getChannelsNameMapInTeam(testState, 'junk'), {}); + }); +}); + +describe('Selectors.Channels.getChannelsWithUnreadSection', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + [user2.id]: user2, + [user3.id]: user3, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + display_name: 'Channel Name', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team2.id), + type: General.PRIVATE_CHANNEL, + display_name: 'Channel Name', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + display_name: 'Channel Name', + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(''), + type: General.DM_CHANNEL, + display_name: 'Channel Name', + name: getDirectChannelName(user.id, user2.id), + }; + const channel5 = { + ...TestHelper.fakeChannelWithId(''), + type: General.GM_CHANNEL, + display_name: [user.username, user2.username, user3.username].join(', '), + name: '', + total_msg_count: 2, + }; + const channel6 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + display_name: 'Channel Name', + }; + const channel7 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.PRIVATE_CHANNEL, + display_name: 'Channel Name', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + [channel6.id]: channel6, + [channel7.id]: channel7, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel3.id, channel6.id, channel7.id], + [team2.id]: [channel2.id], + '': [channel4.id, channel5.id], + }; + + const myMembers = { + [channel1.id]: {msg_count: 1, mention_count: 1}, + [channel3.id]: {msg_count: 0, mention_count: 0}, + [channel4.id]: {msg_count: 0, mention_count: 1}, + [channel5.id]: {msg_count: 1, mention_count: 0}, + [channel6.id]: {msg_count: 0, mention_count: 0}, + [channel7.id]: {msg_count: 0, mention_count: 0}, + }; + + const myPreferences = { + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel1.id}`]: { + value: 'true', + }, + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel3.id}`]: { + value: 'true', + }, + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel4.id}`]: { + value: 'false', + }, + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel5.id}`]: { + value: 'true', + }, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + statuses: {}, + profilesInChannel: { + [channel4.id]: new Set([user.id, user2.id]), + [channel5.id]: new Set([user.id, user2.id, user3.id]), + }, + }, + teams: { + currentTeamId: team1.id, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + channels: { + currentChannelId: channel1.id, + channels, + channelsInTeam, + myMembers, + }, + preferences: { + myPreferences, + }, + general: { + config: {}, + }, + }, + }); + + it('get channels by category including unreads', () => { + const categories = Selectors.getChannelsWithUnreadSection(testState); + const { + unreadChannels, + favoriteChannels, + publicChannels, + privateChannels, + directAndGroupChannels, + } = categories; + + assert.equal(unreadChannels.length, 3); + assert.equal(favoriteChannels.length, 1); + assert.equal(publicChannels.length, 1); + assert.equal(privateChannels.length, 1); + assert.equal(directAndGroupChannels.length, 0); + }); +}); + +describe('Selectors.Channels.getGroupChannels', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const profiles = { + [user.id]: user, + [user2.id]: user2, + [user3.id]: user3, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + display_name: 'Channel Name', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.PRIVATE_CHANNEL, + display_name: 'Channel Name', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(''), + type: General.GM_CHANNEL, + display_name: [user.username, user3.username].join(', '), + name: '', + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(''), + type: General.DM_CHANNEL, + display_name: 'Channel Name', + name: getDirectChannelName(user.id, user2.id), + }; + const channel5 = { + ...TestHelper.fakeChannelWithId(''), + type: General.GM_CHANNEL, + display_name: [user.username, user2.username, user3.username].join(', '), + name: '', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + '': [channel3.id, channel4.id, channel5.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + statuses: {}, + profilesInChannel: { + [channel3.id]: new Set([user.id, user3.id]), + [channel4.id]: new Set([user.id, user2.id]), + [channel5.id]: new Set([user.id, user2.id, user3.id]), + }, + }, + channels: { + channels, + channelsInTeam, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('get group channels', () => { + assert.deepEqual(Selectors.getGroupChannels(testState), [ + {...channel3, display_name: [user3.username].sort(sortUsernames).join(', ')}, + {...channel5, display_name: [user2.username, user3.username].sort(sortUsernames).join(', ')}, + ]); + }); +}); + +describe('Selectors.Channels.getChannelIdsInCurrentTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + const channel3 = TestHelper.fakeChannelWithId(team2.id); + const channel4 = TestHelper.fakeChannelWithId(team2.id); + const channel5 = TestHelper.fakeChannelWithId(''); + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + [team2.id]: [channel3.id, channel4.id], + // eslint-disable-next-line no-useless-computed-key + ['']: [channel5.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channelsInTeam, + }, + }, + }); + + it('get channel ids in current team strict equal', () => { + const newChannel = TestHelper.fakeChannelWithId(team2.id); + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channelsInTeam: { + ...testState.entities.channels.channelsInTeam, + [team2.id]: [ + ...testState.entities.channels.channelsInTeam[team2.id], + newChannel.id, + ], + }, + }, + }, + }; + + const fromOriginalState = Selectors.getChannelIdsInCurrentTeam(testState); + const fromModifiedState = Selectors.getChannelIdsInCurrentTeam(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + + // it should't have a direct channel + assert.equal(fromModifiedState.includes(channel5.id), false, 'should not have direct channel on a team'); + }); +}); + +describe('Selectors.Channels.getChannelIdsForCurrentTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + const channel3 = TestHelper.fakeChannelWithId(team2.id); + const channel4 = TestHelper.fakeChannelWithId(team2.id); + const channel5 = TestHelper.fakeChannelWithId(''); + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + [team2.id]: [channel3.id, channel4.id], + // eslint-disable-next-line no-useless-computed-key + ['']: [channel5.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channelsInTeam, + }, + }, + }); + + it('get channel ids for current team strict equal', () => { + const anotherChannel = TestHelper.fakeChannelWithId(team2.id); + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channelsInTeam: { + ...testState.entities.channels.channelsInTeam, + [team2.id]: [ + ...testState.entities.channels.channelsInTeam[team2.id], + anotherChannel.id, + ], + }, + }, + }, + }; + + const fromOriginalState = Selectors.getChannelIdsForCurrentTeam(testState); + const fromModifiedState = Selectors.getChannelIdsForCurrentTeam(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + + // it should have a direct channel + assert.ok(fromModifiedState.includes(channel5.id)); + }); +}); + +describe('Selectors.Channels.isCurrentChannelFavorite', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const myPreferences = { + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel1.id}`]: { + name: channel1.id, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, + value: 'true', + }, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + currentChannelId: channel1.id, + }, + preferences: { + myPreferences, + }, + }, + }); + + it('isCurrentChannelFavorite', () => { + assert.ok(Selectors.isCurrentChannelFavorite(testState) === true); + + const newState = { + entities: { + channels: { + currentChannelId: channel2.id, + }, + preferences: { + myPreferences, + }, + }, + }; + assert.ok(Selectors.isCurrentChannelFavorite(newState) === false); + }); +}); + +describe('Selectors.Channels.isCurrentChannelMuted', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const myMembers = { + [channel1.id]: {channel_id: channel1.id}, + [channel2.id]: {channel_id: channel2.id, notify_props: {mark_unread: 'mention'}}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + currentChannelId: channel1.id, + myMembers, + }, + }, + }); + + it('isCurrentChannelMuted', () => { + assert.ok(Selectors.isCurrentChannelMuted(testState) === false); + + const newState = { + entities: { + channels: { + ...testState.entities.channels, + currentChannelId: channel2.id, + }, + }, + }; + assert.ok(Selectors.isCurrentChannelMuted(newState) === true); + }); +}); + +describe('Selectors.Channels.isCurrentChannelArchived', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + delete_at: 1, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + currentChannelId: channel1.id, + channels, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('isCurrentChannelArchived', () => { + assert.ok(Selectors.isCurrentChannelArchived(testState) === false); + + const newState = { + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + currentChannelId: channel2.id, + }, + }, + }; + + assert.ok(Selectors.isCurrentChannelArchived(newState) === true); + }); +}); + +describe('Selectors.Channels.isCurrentChannelDefault', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + name: General.DEFAULT_CHANNEL, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + currentChannelId: channel1.id, + channels, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('isCurrentChannelDefault', () => { + assert.ok(Selectors.isCurrentChannelDefault(testState) === false); + + const newState = { + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + currentChannelId: channel2.id, + }, + }, + }; + + assert.ok(Selectors.isCurrentChannelDefault(newState) === true); + }); +}); + +describe('Selectors.Channels.getSortedFavoriteChannelWithUnreadsIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + // eslint-disable-next-line no-useless-computed-key + ['']: [], + }; + + const myMembers = { + [channel1.id]: {channel_id: channel1.id}, + [channel2.id]: {channel_id: channel2.id}, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const myPreferences = { + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel1.id}`]: { + name: channel1.id, + value: 'true', + }, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + currentChannelId: channel1.id, + channels, + myMembers, + channelsInTeam, + }, + users: { + profiles, + currentUserId: user1.id, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences, + }, + general: { + config: {}, + }, + }, + }); + + it('Should not include deleted users in favorites', () => { + const newDmChannel = TestHelper.fakeDmChannel(user1.id, 'newfakeId'); + newDmChannel.total_msg_count = 1; + newDmChannel.display_name = ''; + newDmChannel.name = getDirectChannelName(user1.id, 'newfakeId'); + + const newState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [newDmChannel.id]: newDmChannel, + }, + channelsInTeam: { + ...testState.entities.channels.channelsInTeam, + '': [ + ...testState.entities.channels.channelsInTeam[''], + newDmChannel.id, + ], + }, + myMembers: { + ...testState.entities.channels.myMembers, + [newDmChannel.id]: {channel_id: newDmChannel.id, user_id: user1.id, msg_count: 1, mention_count: 0, notifyProps: {}}, + }, + }, + preferences: { + myPreferences: { + ...testState.entities.preferences.myPreferences, + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${newDmChannel.id}`]: { + name: newDmChannel.id, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, + value: 'true', + }, + [`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--newfakeId`]: { + category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + name: 'newfakeId', + value: 'true', + }, + }, + }, + }, + + }; + const fromOriginalState = Selectors.getSortedFavoriteChannelWithUnreadsIds(testState); + const fromModifiedState = Selectors.getSortedFavoriteChannelWithUnreadsIds(newState); + assert.ok(fromOriginalState.length === 1); + assert.ok(fromModifiedState.length === 1); + }); +}); + +describe('Selectors.Channels.getChannelsWithUserProfiles', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = { + ...TestHelper.fakeChannelWithId(''), + type: General.GM_CHANNEL, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id], + // eslint-disable-next-line no-useless-computed-key + ['']: [channel2.id], + }; + + const user1 = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + [user2.id]: user2, + [user3.id]: user3, + }; + + const profilesInChannel = { + [channel1.id]: new Set([user1.id, user2.id]), + [channel2.id]: new Set([user1.id, user2.id, user3.id]), + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + channels, + channelsInTeam, + }, + users: { + currentUserId: user1.id, + profiles, + profilesInChannel, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('getChannelsWithUserProfiles', () => { + const channelWithUserProfiles = Selectors.getChannelsWithUserProfiles(testState); + assert.equal(channelWithUserProfiles.length, 1); + assert.equal(channelWithUserProfiles[0].profiles.length, 2); + }); +}); + +describe('Selectors.Channels.getRedirectChannelNameForTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const teams = { + [team1.id]: team1, + [team2.id]: team2, + }; + + const myTeamMembers = { + [team1.id]: {}, + [team2.id]: {}, + }; + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + const channel3 = TestHelper.fakeChannelWithId(team1.id); + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const myChannelMembers = { + [channel1.id]: {channel_id: channel1.id}, + [channel2.id]: {channel_id: channel2.id}, + [channel3.id]: {channel_id: channel3.id}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + teams, + myMembers: myTeamMembers, + }, + channels: { + channels, + myMembers: myChannelMembers, + }, + users: { + currentUserId: user1.id, + profiles, + }, + general: {}, + }, + }); + + it('getRedirectChannelNameForTeam without advanced permissions', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + general: { + ...testState.entities.general, + serverVersion: '4.8.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team1.id), General.DEFAULT_CHANNEL); + }); + + it('getRedirectChannelNameForTeam with advanced permissions but without JOIN_PUBLIC_CHANNELS permission', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + 'new-not-member-channel': { + id: 'new-not-member-channel', + display_name: '111111', + name: 'new-not-member-channel', + team_id: team1.id, + }, + [channel1.id]: { + id: channel1.id, + display_name: 'aaaaaa', + name: 'test-channel', + team_id: team1.id, + }, + }, + }, + roles: { + roles: { + system_user: {permissions: []}, + }, + }, + general: { + ...testState.entities.general, + serverVersion: '5.12.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team1.id), 'test-channel'); + }); + + it('getRedirectChannelNameForTeam with advanced permissions and with JOIN_PUBLIC_CHANNELS permission', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + roles: { + roles: { + system_user: {permissions: ['join_public_channels']}, + }, + }, + general: { + ...testState.entities.general, + serverVersion: '5.12.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team1.id), General.DEFAULT_CHANNEL); + }); + + it('getRedirectChannelNameForTeam with advanced permissions but without JOIN_PUBLIC_CHANNELS permission but being member of town-square', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + 'new-not-member-channel': { + id: 'new-not-member-channel', + display_name: '111111', + name: 'new-not-member-channel', + team_id: team1.id, + }, + [channel1.id]: { + id: channel1.id, + display_name: 'Town Square', + name: 'town-square', + team_id: team1.id, + }, + }, + }, + roles: { + roles: { + system_user: {permissions: []}, + }, + }, + general: { + ...testState.entities.general, + serverVersion: '5.12.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team1.id), General.DEFAULT_CHANNEL); + }); + + it('getRedirectChannelNameForTeam without advanced permissions in not current team', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + general: { + ...testState.entities.general, + serverVersion: '4.8.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team2.id), General.DEFAULT_CHANNEL); + }); + + it('getRedirectChannelNameForTeam with advanced permissions but without JOIN_PUBLIC_CHANNELS permission in not current team', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + 'new-not-member-channel': { + id: 'new-not-member-channel', + display_name: '111111', + name: 'new-not-member-channel', + team_id: team2.id, + }, + [channel3.id]: { + id: channel3.id, + display_name: 'aaaaaa', + name: 'test-channel', + team_id: team2.id, + }, + }, + }, + roles: { + roles: { + system_user: {permissions: []}, + }, + }, + general: { + ...testState.entities.general, + serverVersion: '5.12.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team2.id), 'test-channel'); + }); + + it('getRedirectChannelNameForTeam with advanced permissions and with JOIN_PUBLIC_CHANNELS permission in not current team', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + roles: { + roles: { + system_user: {permissions: ['join_public_channels']}, + }, + }, + general: { + ...testState.entities.general, + serverVersion: '5.12.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team2.id), General.DEFAULT_CHANNEL); + }); + + it('getRedirectChannelNameForTeam with advanced permissions but without JOIN_PUBLIC_CHANNELS permission but being member of town-square in not current team', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + 'new-not-member-channel': { + id: 'new-not-member-channel', + display_name: '111111', + name: 'new-not-member-channel', + team_id: team2.id, + }, + [channel3.id]: { + id: channel3.id, + display_name: 'Town Square', + name: 'town-square', + team_id: team2.id, + }, + }, + }, + roles: { + roles: { + system_user: {permissions: []}, + }, + }, + general: { + ...testState.entities.general, + serverVersion: '5.12.0', + }, + }, + }; + assert.equal(Selectors.getRedirectChannelNameForTeam(modifiedState, team2.id), General.DEFAULT_CHANNEL); + }); +}); + +describe('Selectors.Channels.getDirectAndGroupChannels', () => { + const user1 = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(''), + display_name: [user1.username, user2.username, user3.username].join(', '), + type: General.GM_CHANNEL, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(''), + name: getDirectChannelName(user1.id, user2.id), + type: General.DM_CHANNEL, + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(''), + name: getDirectChannelName(user1.id, user3.id), + type: General.DM_CHANNEL, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + }; + + const profiles = { + [user1.id]: user1, + [user2.id]: user2, + [user3.id]: user3, + }; + + const profilesInChannel = { + [channel1.id]: new Set([user1.id, user2.id, user3.id]), + [channel2.id]: new Set([user1.id, user2.id]), + [channel3.id]: new Set([user1.id, user3.id]), + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user1.id, + profiles, + profilesInChannel, + }, + channels: { + channels, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('will return no channels if there is no active user', () => { + const state = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + currentUserId: null, + }, + }, + }; + + assert.deepEqual(Selectors.getDirectAndGroupChannels(state), []); + }); + + it('will return only direct and group message channels', () => { + const state = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + }, + }, + }; + + assert.deepEqual(Selectors.getDirectAndGroupChannels(state), [ + {...channel1, display_name: [user2.username, user3.username].sort(sortUsernames).join(', ')}, + {...channel2, display_name: user2.username}, + {...channel3, display_name: user3.username}, + ]); + }); + + it('will not error out on undefined channels', () => { + const state = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + }, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + ['undefined']: undefined, //eslint-disable-line no-useless-computed-key + }, + }, + }, + }; + + assert.deepEqual(Selectors.getDirectAndGroupChannels(state), [ + {...channel1, display_name: [user2.username, user3.username].sort(sortUsernames).join(', ')}, + {...channel2, display_name: user2.username}, + {...channel3, display_name: user3.username}, + ]); + }); +}); + +describe('Selectors.Channels.getOrderedChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + last_post_at: 0, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + last_post_at: 0, + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + last_post_at: 0, + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + last_post_at: 0, + }; + const channel5 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + last_post_at: 0, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id, channel3.id, channel4.id, channel5.id], + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const myChannelMembers = { + [channel1.id]: {}, + [channel2.id]: {}, + [channel3.id]: {}, + [channel4.id]: {}, + [channel5.id]: {}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + myMembers: myChannelMembers, + channelsInTeam, + }, + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + it('get ordered channel ids by_type in current team strict equal', () => { + const chan5 = {...testState.entities.channels.channels[channel5.id]}; + chan5.header = 'This should not change the results'; + + const sidebarPrefs = { + grouping: 'by_type', + sorting: 'alpha', + unreads_at_top: 'true', + favorite_at_top: 'true', + }; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel5.id]: chan5, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getOrderedChannelIds( + testState, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + const fromModifiedState = Selectors.getOrderedChannelIds( + modifiedState, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + assert.deepEqual(fromOriginalState, fromModifiedState); + + chan5.total_msg_count = 10; + + const unreadChannelState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + channels: { + ...modifiedState.entities.channels.channels, + [channel5.id]: chan5, + }, + myMembers: { + ...modifiedState.entities.channels.myMembers, + [channel5.id]: { + ...modifiedState.entities.channels.myMembers[channel5.id], + mention_count: 1, + }, + }, + }, + }, + }; + + const fromUnreadState = Selectors.getOrderedChannelIds( + unreadChannelState, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + assert.notDeepEqual(fromModifiedState, fromUnreadState); + + const favoriteChannelState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + preferences: { + ...modifiedState.entities.preferences, + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel4.id}`]: { + name: channel4.id, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, + value: 'true', + }, + }, + }, + }; + + const fromFavoriteState = Selectors.getOrderedChannelIds( + favoriteChannelState, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + assert.notDeepEqual(fromUnreadState, fromFavoriteState); + }); + + it('get ordered channel ids by recency order in current team strict equal', () => { + const chan2 = {...testState.entities.channels.channels[channel2.id]}; + chan2.header = 'This should not change the results'; + + const sidebarPrefs = { + grouping: 'never', + sorting: 'recent', + unreads_at_top: 'false', + favorite_at_top: 'false', + }; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getOrderedChannelIds( + testState, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + const fromModifiedState = Selectors.getOrderedChannelIds( + modifiedState, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + assert.deepEqual(fromOriginalState, fromModifiedState); + + chan2.last_post_at = (new Date()).getTime() + 500; + const recencyInChan2State = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + channels: { + ...modifiedState.entities.channels.channels, + [chan2.id]: chan2, + }, + }, + }, + }; + + const fromRecencyInChan2State = Selectors.getOrderedChannelIds( + recencyInChan2State, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + assert.notDeepEqual(fromModifiedState, fromRecencyInChan2State); + assert.ok(fromRecencyInChan2State[0].items[0] === chan2.id); + + const chan3 = {...testState.entities.channels.channels[channel3.id]}; + chan3.last_post_at = (new Date()).getTime() + 500; + const recencyInChan3State = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + channels: { + ...modifiedState.entities.channels.channels, + [channel1.id]: chan3, + }, + }, + }, + }; + + const fromRecencyInChan3State = Selectors.getOrderedChannelIds( + recencyInChan3State, + null, + sidebarPrefs.grouping, + sidebarPrefs.sorting, + sidebarPrefs.unreads_at_top === 'true', + sidebarPrefs.favorite_at_top === 'true', + ); + + assert.notDeepEqual(fromRecencyInChan2State, fromRecencyInChan3State); + assert.ok(fromRecencyInChan3State[0].items[0] === chan3.id); + }); +}); + +describe('Selectors.Channels.canManageAnyChannelMembersInCurrentTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.OPEN_CHANNEL, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.PRIVATE_CHANNEL, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const myChannelMembers = { + [channel1.id]: {}, + [channel2.id]: {}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + myMembers: myChannelMembers, + }, + users: { + profiles, + currentUserId: user1.id, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('will return false if channel_user does not have permissions to manage channel members', () => { + const newState = { + entities: { + ...testState.entities, + roles: { + roles: { + channel_user: { + permissions: [], + }, + }, + }, + channels: { + ...testState.entities.channels, + myMembers: { + ...testState.entities.channels.myMembers, + [channel1.id]: { + ...testState.entities.channels.myMembers[channel1.id], + roles: 'channel_user', + }, + [channel2.id]: { + ...testState.entities.channels.myMembers[channel2.id], + roles: 'channel_user', + }, + }, + }, + }, + }; + + assert.ok(Selectors.canManageAnyChannelMembersInCurrentTeam(newState) === false); + }); + + it('will return true if channel_user has permissions to manage public channel members', () => { + const newState = { + entities: { + ...testState.entities, + roles: { + roles: { + channel_user: { + permissions: ['manage_public_channel_members'], + }, + }, + }, + channels: { + ...testState.entities.channels, + myMembers: { + ...testState.entities.channels.myMembers, + [channel1.id]: { + ...testState.entities.channels.myMembers[channel1.id], + roles: 'channel_user', + }, + [channel2.id]: { + ...testState.entities.channels.myMembers[channel2.id], + roles: 'channel_user', + }, + }, + }, + }, + }; + + assert.ok(Selectors.canManageAnyChannelMembersInCurrentTeam(newState) === true); + }); + + it('will return true if channel_user has permissions to manage private channel members', () => { + const newState = { + entities: { + ...testState.entities, + roles: { + roles: { + channel_user: { + permissions: ['manage_private_channel_members'], + }, + }, + }, + channels: { + ...testState.entities.channels, + myMembers: { + ...testState.entities.channels.myMembers, + [channel1.id]: { + ...testState.entities.channels.myMembers[channel1.id], + roles: 'channel_user', + }, + [channel2.id]: { + ...testState.entities.channels.myMembers[channel2.id], + roles: 'channel_user', + }, + }, + }, + }, + }; + + assert.ok(Selectors.canManageAnyChannelMembersInCurrentTeam(newState) === true); + }); + + it('will return false if channel admins have permissions, but the user is not a channel admin of any channel', () => { + const newState = { + entities: { + ...testState.entities, + roles: { + roles: { + channel_admin: { + permissions: ['manage_public_channel_members'], + }, + }, + }, + channels: { + ...testState.entities.channels, + myMembers: { + ...testState.entities.channels.myMembers, + [channel1.id]: { + ...testState.entities.channels.myMembers[channel1.id], + roles: 'channel_user', + }, + [channel2.id]: { + ...testState.entities.channels.myMembers[channel2.id], + roles: 'channel_user', + }, + }, + }, + }, + }; + + assert.ok(Selectors.canManageAnyChannelMembersInCurrentTeam(newState) === false); + }); + + it('will return true if channel admins have permission, and the user is a channel admin of some channel', () => { + const newState = { + entities: { + ...testState.entities, + roles: { + roles: { + channel_admin: { + permissions: ['manage_public_channel_members'], + }, + }, + }, + channels: { + ...testState.entities.channels, + myMembers: { + ...testState.entities.channels.myMembers, + [channel1.id]: { + ...testState.entities.channels.myMembers[channel1.id], + roles: 'channel_user channel_admin', + }, + [channel2.id]: { + ...testState.entities.channels.myMembers[channel2.id], + roles: 'channel_user', + }, + }, + }, + }, + }; + + assert.ok(Selectors.canManageAnyChannelMembersInCurrentTeam(newState) === true); + }); + + it('will return true if team admins have permission, and the user is a team admin', () => { + const newState = { + entities: { + ...testState.entities, + roles: { + roles: { + team_admin: { + permissions: ['manage_public_channel_members'], + }, + }, + }, + users: { + ...testState.entities.users, + profiles: { + ...testState.entities.users.profiles, + [user1.id]: { + ...testState.entities.users.profiles[user1.id], + roles: 'team_admin', + }, + }, + }, + }, + }; + + assert.ok(Selectors.canManageAnyChannelMembersInCurrentTeam(newState) === true); + }); +}); + +describe('Selectors.Channels.filterPostIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + channels, + }, + posts: { + posts: {}, + }, + }, + }); + + it('filters post IDs by the given condition', () => { + const posts = { + a: {id: 'a', channel_id: channel1.id, create_at: 1, user_id: user1.id}, + b: {id: 'b', channel_id: channel2.id, create_at: 2, user_id: user1.id}, + c: {id: 'c', root_id: 'a', channel_id: channel1.id, create_at: 3, user_id: 'b'}, + }; + const testStateC = JSON.parse(JSON.stringify(testState)); + testStateC.entities.posts.posts = posts; + testStateC.entities.channels.channels[channel2.id].delete_at = 1; + + const filterPostIDsByArchived = Selectors.filterPostIds((channel) => channel.delete_at !== 0); + const filterPostIDsByUserB = Selectors.filterPostIds((channel, post) => post.user_id === 'b'); + + const filterPostIDsInvalid = Selectors.filterPostIds((channel, post) => foo === 'b'); // eslint-disable-line + let filterErrorMessage; + try { + const result = ['bar'].filter((item) => foo === 'b'); // eslint-disable-line + } catch (e) { + filterErrorMessage = e.message; + } + + const postIDs = Object.keys(posts); + + assert.deepEqual(filterPostIDsByArchived(testStateC, postIDs), ['b']); + assert.deepEqual(filterPostIDsByUserB(testStateC, postIDs), ['c']); + + assert.throws(() => Selectors.filterPostIds(), TypeError); + + assert.throws(() => filterPostIDsInvalid(testStateC, postIDs), ReferenceError, filterErrorMessage); + }); +}); + +describe('Selectors.Channels.getSortedPrivateChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.PRIVATE_CHANNEL, + display_name: 'DEF', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + type: General.PRIVATE_CHANNEL, + display_name: 'GHI', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + }; + + const myChannelMembers = { + [channel1.id]: {}, + [channel2.id]: {}, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers: myChannelMembers, + }, + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + it('get sorted private channel ids in current team strict equal', () => { + const chan2 = {...testState.entities.channels.channels[channel2.id]}; + chan2.header = 'This should not change the results'; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getSortedPrivateChannelIds(testState); + const fromModifiedState = Selectors.getSortedPrivateChannelIds(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + assert.ok(fromModifiedState[0] === channel1.id); + + chan2.display_name = 'abc'; + const updateState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + channels: { + ...modifiedState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromUpdateState = Selectors.getSortedPrivateChannelIds(updateState); + assert.ok(fromModifiedState !== fromUpdateState); + assert.ok(fromUpdateState[0] === channel2.id); + }); +}); + +describe('Selectors.Channels.getSortedPublicChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'DEF', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'GHI', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + }; + + const myChannelMembers = { + [channel1.id]: {}, + [channel2.id]: {}, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers: myChannelMembers, + }, + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + it('get sorted public channel ids in current team strict equal', () => { + const chan2 = {...testState.entities.channels.channels[channel2.id]}; + chan2.header = 'This should not change the results'; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getSortedPublicChannelIds(testState); + const fromModifiedState = Selectors.getSortedPublicChannelIds(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + assert.ok(fromModifiedState[0] === channel1.id); + + chan2.display_name = 'abc'; + const updateState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + channels: { + ...modifiedState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromUpdateState = Selectors.getSortedPublicChannelIds(updateState); + assert.ok(fromModifiedState !== fromUpdateState); + assert.ok(fromUpdateState[0] === channel2.id); + }); +}); + +describe('Selectors.Channels.getSortedFavoriteChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'DEF', + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'GHI', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + }; + + const myChannelMembers = { + [channel1.id]: {notify_props: {}}, + [channel2.id]: {notify_props: {}}, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const myPreferences = { + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel1.id}`]: { + name: channel1.id, + value: 'true', + }, + [`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channel2.id}`]: { + name: channel2.id, + value: 'true', + }, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers: myChannelMembers, + }, + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences, + }, + general: { + config: {}, + }, + }, + }); + + it('get sorted favorite channel ids in current team strict equal', () => { + const chan1 = {...testState.entities.channels.channels[channel1.id]}; + chan1.total_msg_count = 10; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel1.id]: chan1, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getSortedFavoriteChannelIds(testState); + const fromModifiedState = Selectors.getSortedFavoriteChannelIds(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + assert.ok(fromModifiedState[0] === channel1.id); + + const chan2 = {...testState.entities.channels.channels[channel2.id]}; + chan2.display_name = 'abc'; + + const updateState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + channels: { + ...modifiedState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromUpdateState = Selectors.getSortedFavoriteChannelIds(updateState); + assert.ok(fromModifiedState !== fromUpdateState); + assert.ok(fromUpdateState[0] === channel2.id); + }); +}); + +describe('Selectors.Channels.getSortedUnreadChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'DEF', + }; + const channel2 = TestHelper.fakeChannelWithId(team1.id); + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'ABC', + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id, channel3.id], + }; + + const myChannelMembers = { + [channel1.id]: {mention_count: 1, msg_count: 0, notify_props: {}}, + [channel2.id]: {mention_count: 0, msg_count: 0, notify_props: {}}, + [channel3.id]: {mention_count: 0, msg_count: 0, notify_props: {}}, + }; + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + myMembers: myChannelMembers, + channelsInTeam, + }, + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('get sorted unread channel ids in current team strict equal', () => { + const chan1 = {...testState.entities.channels.channels[channel1.id]}; + chan1.total_msg_count = 10; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel1.id]: chan1, + }, + }, + }, + }; + + // When adding a mention to channel3 with display_name 'ABC' states are !== and channel3 is above all others + const mentionState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + myMembers: { + ...modifiedState.entities.channels.myMembers, + [channel3.id]: { + ...modifiedState.entities.channels.myMembers[channel3.id], + mention_count: 1, + }, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getSortedUnreadChannelIds(testState); + const fromModifiedState = Selectors.getSortedUnreadChannelIds(modifiedState); + const fromMentionState = Selectors.getSortedUnreadChannelIds(mentionState); + + // mentions should be prioritized to the top + assert.ok(fromOriginalState === fromModifiedState); + assert.ok(fromMentionState !== fromModifiedState); + + // channel3 and channel1 are above all others + // since default order is "alpha", channel3 with display_name "ABC" should come first + assert.ok(fromMentionState[0] === channel3.id); + + // followed by channel1 with display_name "DEF" + assert.ok(fromMentionState[1] === channel1.id); + + const hasMentionMutedChannelState = { + ...mentionState, + entities: { + ...mentionState.entities, + channels: { + ...mentionState.entities.channels, + myMembers: { + ...mentionState.entities.channels.myMembers, + [channel3.id]: { + mention_count: 1, + notify_props: { + mark_unread: 'mention', + }, + }, + }, + }, + }, + }; + + const fromHasMentionMutedChannelState = Selectors.getSortedUnreadChannelIds(hasMentionMutedChannelState); + + // For channels with mentions, non-muted channel1 should come first before muted channel3. + assert.ok(fromHasMentionMutedChannelState[0] === channel1.id); + assert.ok(fromHasMentionMutedChannelState[1] === channel3.id); + }); +}); + +describe('Selectors.Channels.getUnreadChannelIds', () => { + const user1 = TestHelper.fakeUserWithId(); + user1.username = 'user'; + const user2 = TestHelper.fakeUserWithId(); + user2.username = 'user2'; + const user3 = TestHelper.fakeUserWithId(); + user3.username = 'user3'; + const fakeUser = TestHelper.fakeUserWithId('fakeUserId'); + fakeUser.username = 'fakeUser'; + const profiles = { + [fakeUser.id]: fakeUser, + [user1.id]: user1, + [user2.id]: user2, + [user3.id]: user3, + }; + + const team1 = TestHelper.fakeTeamWithId(); + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'ABC', + total_msg_count: 2, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'DEF', + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + display_name: 'GHI', + total_msg_count: 2, + }; + const channel4 = { + ...TestHelper.fakeChannelWithId(''), + display_name: [user1.username, user2.username, user3.username].join(', '), + type: General.GM_CHANNEL, + total_msg_count: 3, + last_post_at: Date.now(), + }; + const channel5 = { + ...TestHelper.fakeDmChannel(user1.id, 'fakeUserId'), + total_msg_count: 3, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + [channel4.id]: channel4, + [channel5.id]: channel5, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id, channel3.id], + // eslint-disable-next-line no-useless-computed-key + ['']: [channel4.id], + }; + + const myChannelMembers = { + [channel1.id]: {mention_count: 0, msg_count: 1, notify_props: {}}, + [channel2.id]: {mention_count: 1, msg_count: 1, notify_props: {}}, + [channel3.id]: {mention_count: 1, msg_count: 0, notify_props: {}}, + [channel4.id]: {mention_count: 0, msg_count: 2, notify_props: {}}, + [channel5.id]: {mention_count: 2, msg_count: 3, notify_props: {}}, + }; + const membersInChannel = { + [channel4.id]: { + [user1.id]: {channel_id: channel4.id, user_id: user1.id}, + }, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + myMembers: myChannelMembers, + channelsInTeam, + membersInChannel, + }, + users: { + currentUserId: user1.id, + profiles, + profilesInChannel: { + [channel4.id]: null, + }, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + test('channels should be sorted alphabetically with mentions coming first', () => { + expect(Selectors.getSortedUnreadChannelIds(testState)).toEqual([ + channel2.id, // has mention and display name is "DEF" + channel3.id, // has mention and display name is "GHI" + channel1.id, // has unread and display name is "ABC" + channel4.id, // has unread and display name is "user, user2, user3" + ]); + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + myMembers: { + ...testState.entities.channels.myMembers, + [channel1.id]: { + ...testState.entities.channels.myMembers[channel1.id], + mention_count: 1, + }, + }, + }, + }, + }; + + expect(Selectors.getSortedUnreadChannelIds(modifiedState)).toEqual([ + channel1.id, // has mention and display name is "ABC" + channel2.id, // has mention and display name is "DEF" + channel3.id, // has mention and display name is "GHI" + channel4.id, // has unread and display name is "user, user2, user3" + ]); + }); + + test('selector should return the same array as long as the order stays the same', () => { + const fromOriginalState = Selectors.getSortedUnreadChannelIds(testState); + + // Adding messages to an already unread channel shouldn't change the order + let modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel2.id]: { + ...testState.entities.channels.channels[channel2.id], + total_msg_count: 10, + }, + }, + }, + }, + }; + + expect(Selectors.getSortedUnreadChannelIds(modifiedState)).toBe(fromOriginalState); + + // Adding a mention to a channel that didn't have mentions before should change the order + modifiedState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + channels: { + ...modifiedState.entities.channels, + myMembers: { + ...modifiedState.entities.channels.myMembers, + [channel4.id]: { + ...modifiedState.entities.channels.myMembers[channel4.id], + mention_count: 1, + }, + }, + }, + }, + }; + + expect(Selectors.getSortedUnreadChannelIds(modifiedState)).not.toBe(fromOriginalState); + }); +}); + +describe('Selectors.Channels.getUnreadChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + }; + + const myChannelMembers = { + [channel1.id]: {}, + [channel2.id]: {}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + channels, + channelsInTeam, + myMembers: myChannelMembers, + }, + }, + }); + it('get unread channel ids in current team strict equal', () => { + const chan2 = {...testState.entities.channels.channels[channel2.id]}; + chan2.total_msg_count = 10; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getUnreadChannelIds(testState); + const fromModifiedState = Selectors.getUnreadChannelIds(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + }); + + it('get unread channel ids in current team and keep specified channel as unread', () => { + const chan2 = {...testState.entities.channels.channels[channel2.id]}; + chan2.total_msg_count = 10; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel2.id]: chan2, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getUnreadChannelIds(testState); + const fromModifiedState = Selectors.getUnreadChannelIds(modifiedState, {id: channel1.id}); + + assert.ok(fromOriginalState !== fromModifiedState); + assert.ok(fromModifiedState.includes(channel1.id)); + }); +}); + +describe('Selectors.Channels.getDirectChannelIds', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const user1 = TestHelper.fakeUserWithId(); + + const profiles = { + [user1.id]: user1, + }; + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const myChannelMembers = { + [channel1.id]: {}, + [channel2.id]: {}, + }; + + const profilesInChannel = { + [channel1.id]: new Set([user1.id]), + [channel2.id]: new Set([user1.id]), + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + channels: { + channels, + myMembers: myChannelMembers, + }, + users: { + currentUserId: user1.id, + profiles, + profilesInChannel, + }, + posts: { + posts: {}, + postsInChannel: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + it('get direct channel ids strict equal', () => { + const chan1 = {...testState.entities.channels.channels[channel1.id]}; + chan1.total_msg_count += 1; // no reason to set it to 1, this is just to make sure the state changed + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [channel1.id]: chan1, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getDirectChannelIds(testState); + const fromModifiedState = Selectors.getDirectChannelIds(modifiedState); + + assert.ok(fromOriginalState === fromModifiedState); + + // it should't have a channel that belongs to a team + assert.equal(fromModifiedState.includes(channel1.id), false, 'should not have a channel that belongs to a team'); + }); + + it('get direct channel ids for channels with non-null values', () => { + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + const dmChannel1 = TestHelper.fakeDmChannel(user1.id, user2.id); + const dmChannel2 = TestHelper.fakeDmChannel(user1.id, user3.id); + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profiles: { + ...testState.entities.users.profiles, + [user2.id]: user2, + [user3.id]: user3, + }, + }, + channels: { + ...testState.entities.channels, + channels: { + ...testState.entities.channels.channels, + [dmChannel1.id]: dmChannel1, + [dmChannel2.id]: null, + }, + }, + preferences: { + ...testState.entities.preferences, + myPreferences: { + [`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${dmChannel1.teammate_id}`]: { + category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + name: dmChannel1.teammate_id, + value: 'true', + }, + [`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${dmChannel2.teammate_id}`]: { + category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + name: dmChannel2.teammate_id, + value: 'true', + }, + }, + }, + }, + }; + + const fromModifiedState = Selectors.getDirectChannelIds(modifiedState); + assert.equal(fromModifiedState.length, 1); + assert.equal(fromModifiedState[0], dmChannel1.id); + }); +}); + +describe('Selectors.Channels.getUnreadsInCurrentTeam', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + total_msg_count: 2, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + total_msg_count: 8, + }; + const channel3 = { + ...TestHelper.fakeChannelWithId(team1.id), + total_msg_count: 5, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + [channel3.id]: channel3, + }; + + const myChannelMembers = { + [channel1.id]: {notify_props: {}, mention_count: 1, msg_count: 0}, + [channel2.id]: {notify_props: {}, mention_count: 4, msg_count: 0}, + [channel3.id]: {notify_props: {}, mention_count: 4, msg_count: 5}, + }; + + const channelsInTeam = { + [team1.id]: [channel1.id, channel2.id], + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + channels: { + currentChannelId: channel1.id, + channels, + channelsInTeam, + myMembers: myChannelMembers, + }, + users: { + profiles: {}, + }, + preferences: { + myPreferences: {}, + }, + general: { + config: {}, + }, + }, + }); + + it('get unreads for current team', () => { + assert.deepEqual(Selectors.getUnreadsInCurrentTeam(testState), {mentionCount: 4, messageCount: 1}); + }); + + it('get unreads for current read channel', () => { + const testState2 = {...testState, + entities: {...testState.entities, + channels: {...testState.entities.channels, + currentChannelId: channel3.id, + }, + }, + }; + assert.equal(Selectors.countCurrentChannelUnreadMessages(testState2), 0); + }); + + it('get unreads for current unread channel', () => { + assert.equal(Selectors.countCurrentChannelUnreadMessages(testState), 2); + }); + + it('get unreads for channel not on members', () => { + const testState2 = {...testState, + entities: {...testState.entities, + channels: {...testState.entities.channels, + currentChannelId: 'some_other_id', + }, + }, + }; + assert.equal(Selectors.countCurrentChannelUnreadMessages(testState2), 0); + }); + + it('get unreads with a missing profile entity', () => { + const newProfiles = { + ...testState.entities.users.profiles, + }; + Reflect.deleteProperty(newProfiles, 'fakeUserId'); + const newState = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profiles: newProfiles, + }, + }, + }; + + assert.deepEqual(Selectors.getUnreadsInCurrentTeam(newState), {mentionCount: 4, messageCount: 1}); + }); + + it('get unreads with a deactivated user', () => { + const newProfiles = { + ...testState.entities.users.profiles, + fakeUserId: { + ...testState.entities.users.profiles.fakeUserId, + delete_at: 100, + }, + }; + + const newState = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profiles: newProfiles, + }, + }, + }; + assert.deepEqual(Selectors.getUnreadsInCurrentTeam(newState), {mentionCount: 4, messageCount: 1}); + }); + + it('get unreads with a deactivated channel', () => { + const newChannels = { + ...testState.entities.channels.channels, + [channel2.id]: { + ...testState.entities.channels.channels[channel2.id], + delete_at: 100, + }, + }; + + const newState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: newChannels, + }, + }, + }; + + assert.deepEqual(Selectors.getUnreadsInCurrentTeam(newState), {mentionCount: 0, messageCount: 0}); + }); +}); + +describe('Selectors.Channels.getUnreads', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const teams = { + [team1.id]: team1, + [team2.id]: team2, + }; + + const myTeamMembers = { + [team1.id]: {mention_count: 16, msg_count: 32}, + [team2.id]: {mention_count: 64, msg_count: 128}, + }; + + const channel1 = { + ...TestHelper.fakeChannelWithId(team1.id), + total_msg_count: 2, + }; + const channel2 = { + ...TestHelper.fakeChannelWithId(team1.id), + total_msg_count: 8, + }; + + const channels = { + [channel1.id]: channel1, + [channel2.id]: channel2, + }; + + const myChannelMembers = { + [channel1.id]: {notify_props: {}, mention_count: 1, msg_count: 0}, + [channel2.id]: {notify_props: {}, mention_count: 4, msg_count: 0}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + teams, + myMembers: myTeamMembers, + }, + channels: { + channels, + myMembers: myChannelMembers, + }, + users: { + profiles: {}, + }, + }, + }); + + it('get unreads', () => { + assert.deepEqual(Selectors.getUnreads(testState), {mentionCount: 69, messageCount: 130}); + }); + + it('get unreads with a missing profile entity', () => { + const newProfiles = { + ...testState.entities.users.profiles, + }; + Reflect.deleteProperty(newProfiles, 'fakeUserId'); + const newState = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profiles: newProfiles, + }, + }, + }; + + assert.deepEqual(Selectors.getUnreads(newState), {mentionCount: 69, messageCount: 130}); + }); + + it('get unreads with a deactivated user', () => { + const newProfiles = { + ...testState.entities.users.profiles, + fakeUserId: { + ...testState.entities.users.profiles.fakeUserId, + delete_at: 100, + }, + }; + + const newState = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profiles: newProfiles, + }, + }, + }; + assert.deepEqual(Selectors.getUnreads(newState), {mentionCount: 69, messageCount: 130}); + }); + + it('get unreads with a deactivated channel', () => { + const newChannels = { + ...testState.entities.channels.channels, + [channel2.id]: { + ...testState.entities.channels.channels[channel2.id], + delete_at: 100, + }, + }; + + const newState = { + ...testState, + entities: { + ...testState.entities, + channels: { + ...testState.entities.channels, + channels: newChannels, + }, + }, + }; + + assert.deepEqual(Selectors.getUnreads(newState), {mentionCount: 65, messageCount: 129}); + }); +}); + +describe('Selectors.Channels.getUnreads', () => { + const team1 = {id: 'team1', delete_at: 0}; + const team2 = {id: 'team2', delete_at: 0}; + + const channelA = {id: 'channelA', name: 'channelA', team_id: 'team1', total_msg_count: 11, delete_at: 0}; + const channelB = {id: 'channelB', name: 'channelB', team_id: 'team1', total_msg_count: 13, delete_at: 0}; + const channelC = {id: 'channelB', name: 'channelB', team_id: 'team2', total_msg_count: 17, delete_at: 0}; + + const dmChannel = {id: 'dmChannel', name: 'user1__user2', team_id: '', total_msg_count: 11, delete_at: 0, type: General.DM_CHANNEL}; + const gmChannel = {id: 'gmChannel', name: 'gmChannel', team_id: 'team1', total_msg_count: 11, delete_at: 0, type: General.GM_CHANNEL}; + + test('should return the correct number of messages and mentions from channels on the current team', () => { + const myMemberA = {mention_count: 2, msg_count: 3, notify_props: {mark_unread: 'all'}}; + const myMemberB = {mention_count: 5, msg_count: 7, notify_props: {mark_unread: 'all'}}; + + const state = { + entities: { + channels: { + channels: { + channelA, + channelB, + }, + myMembers: { + channelA: myMemberA, + channelB: myMemberB, + }, + }, + teams: { + currentTeamId: 'team1', + myMembers: {}, + teams: { + team1, + }, + }, + users: { + currentUserId: 'user1', + profiles: {}, + }, + }, + }; + + const {messageCount, mentionCount} = Selectors.getUnreads(state); + + expect(messageCount).toBe(2); // channelA and channelB are unread + expect(mentionCount).toBe(myMemberA.mention_count + myMemberB.mention_count); + }); + + test('should not count messages from channel with mark_unread set to "mention"', () => { + const myMemberA = {mention_count: 2, msg_count: 3, notify_props: {mark_unread: 'mention'}}; + const myMemberB = {mention_count: 5, msg_count: 7, notify_props: {mark_unread: 'all'}}; + + const state = { + entities: { + channels: { + channels: { + channelA, + channelB, + }, + myMembers: { + channelA: myMemberA, + channelB: myMemberB, + }, + }, + teams: { + currentTeamId: 'team1', + myMembers: {}, + teams: { + team1, + }, + }, + users: { + currentUserId: 'user1', + profiles: {}, + }, + }, + }; + + const {messageCount, mentionCount} = Selectors.getUnreads(state); + + expect(messageCount).toBe(1); // channelA and channelB are unread, but only channelB is counted because of its mark_unread + expect(mentionCount).toBe(myMemberA.mention_count + myMemberB.mention_count); + }); + + test('should count mentions from DM channels', () => { + const dmMember = {mention_count: 2, msg_count: 3, notify_props: {mark_unread: 'all'}}; + + const state = { + entities: { + channels: { + channels: { + dmChannel, + }, + myMembers: { + dmChannel: dmMember, + }, + }, + teams: { + currentTeamId: 'team1', + myMembers: {}, + teams: { + team1, + }, + }, + users: { + currentUserId: 'user1', + profiles: { + user2: {delete_at: 0}, + }, + }, + }, + }; + + const {messageCount, mentionCount} = Selectors.getUnreads(state); + + expect(messageCount).toBe(1); // dmChannel is unread + expect(mentionCount).toBe(dmMember.mention_count); + }); + + test('should not count mentions from DM channel with archived user', () => { + const dmMember = {mention_count: 2, msg_count: 3, notify_props: {mark_unread: 'all'}}; + + const state = { + entities: { + channels: { + channels: { + dmChannel, + }, + myMembers: { + dmChannel: dmMember, + }, + }, + teams: { + currentTeamId: 'team1', + myMembers: {}, + teams: { + team1, + }, + }, + users: { + currentUserId: 'user1', + profiles: { + user2: {delete_at: 1}, + }, + }, + }, + }; + + const {messageCount, mentionCount} = Selectors.getUnreads(state); + + expect(messageCount).toBe(0); + expect(mentionCount).toBe(0); + }); + + test('should count mentions from GM channels', () => { + const gmMember = {mention_count: 2, msg_count: 3, notify_props: {mark_unread: 'all'}}; + + const state = { + entities: { + channels: { + channels: { + gmChannel, + }, + myMembers: { + gmChannel: gmMember, + }, + }, + teams: { + currentTeamId: 'team1', + myMembers: {}, + teams: { + team1, + }, + }, + users: { + currentUserId: 'user1', + profiles: {}, + }, + }, + }; + + const {messageCount, mentionCount} = Selectors.getUnreads(state); + + expect(messageCount).toBe(1); // gmChannel is unread + expect(mentionCount).toBe(gmMember.mention_count); + }); + + test('should count mentions and messages for other teams from team members', () => { + const myMemberA = {mention_count: 2, msg_count: 3, notify_props: {mark_unread: 'all'}}; + const myMemberC = {mention_count: 5, msg_count: 7, notify_props: {mark_unread: 'all'}}; + + const teamMember1 = {msg_count: 1, mention_count: 2}; + const teamMember2 = {msg_count: 3, mention_count: 6}; + + const state = { + entities: { + channels: { + channels: { + channelA, + channelC, + }, + myMembers: { + channelA: myMemberA, + channelC: myMemberC, + }, + }, + teams: { + currentTeamId: 'team1', + myMembers: { + team1: teamMember1, + team2: teamMember2, + }, + teams: { + team1, + team2, + }, + }, + users: { + currentUserId: 'user1', + profiles: {}, + }, + }, + }; + + const {messageCount, mentionCount} = Selectors.getUnreads(state); + + expect(messageCount).toBe(4); // channelA and channelC are unread + expect(mentionCount).toBe(myMemberA.mention_count + teamMember2.mention_count); + }); +}); + +describe('Selectors.Channels.getMyFirstChannelForTeams', () => { + test('should return the first channel in each team', () => { + const state = { + entities: { + channels: { + channels: { + channelA: {id: 'channelA', name: 'channelA', team_id: 'team1'}, + channelB: {id: 'channelB', name: 'channelB', team_id: 'team2'}, + channelC: {id: 'channelC', name: 'channelC', team_id: 'team1'}, + }, + myMembers: { + channelA: {}, + channelB: {}, + channelC: {}, + }, + }, + teams: { + myMembers: { + team1: {}, + team2: {}, + }, + teams: { + team1: {id: 'team1', delete_at: 0}, + team2: {id: 'team2', delete_at: 0}, + }, + }, + users: { + currentUserId: 'user', + profiles: { + user: {}, + }, + }, + }, + }; + + expect(Selectors.getMyFirstChannelForTeams(state)).toEqual({ + team1: state.entities.channels.channels.channelA, + team2: state.entities.channels.channels.channelB, + }); + }); + + test('should only return channels that the current user is a member of', () => { + const state = { + entities: { + channels: { + channels: { + channelA: {id: 'channelA', name: 'channelA', team_id: 'team1'}, + channelB: {id: 'channelB', name: 'channelB', team_id: 'team1'}, + }, + myMembers: { + channelB: {}, + }, + }, + teams: { + myMembers: { + team1: {}, + }, + teams: { + team1: {id: 'team1', delete_at: 0}, + }, + }, + users: { + currentUserId: 'user', + profiles: { + user: {}, + }, + }, + }, + }; + + expect(Selectors.getMyFirstChannelForTeams(state)).toEqual({ + team1: state.entities.channels.channels.channelB, + }); + }); + + test('should only return teams that the current user is a member of', () => { + const state = { + entities: { + channels: { + channels: { + channelA: {id: 'channelA', name: 'channelA', team_id: 'team1'}, + channelB: {id: 'channelB', name: 'channelB', team_id: 'team2'}, + }, + myMembers: { + channelA: {}, + channelB: {}, + }, + }, + teams: { + myMembers: { + team1: {}, + }, + teams: { + team1: {id: 'team1', delete_at: 0}, + team2: {id: 'team2', delete_at: 0}, + }, + }, + users: { + currentUserId: 'user', + profiles: { + user: {}, + }, + }, + }, + }; + + expect(Selectors.getMyFirstChannelForTeams(state)).toEqual({ + team1: state.entities.channels.channels.channelA, + }); + }); +}); + +test('Selectors.Channels.isManuallyUnread', () => { + const state = { + entities: { + channels: { + manuallyUnread: { + channel1: true, + channel2: false, + }, + }, + }, + }; + + assert.equal(Selectors.isManuallyUnread(state, 'channel1'), true); + assert.equal(Selectors.isManuallyUnread(state, undefined), false); + assert.equal(Selectors.isManuallyUnread(state, 'channel2'), false); + assert.equal(Selectors.isManuallyUnread(state, 'channel3'), false); +}); + +test('Selectors.Channels.getChannelModerations', () => { + const moderations = [{ + name: Permissions.CHANNEL_MODERATED_PERMISSIONS.CREATE_POST, + roles: { + members: true, + }, + }]; + + const state = { + entities: { + channels: { + channelModerations: { + channel1: moderations, + }, + }, + }, + }; + + assert.equal(Selectors.getChannelModerations(state, 'channel1'), moderations); + assert.equal(Selectors.getChannelModerations(state, undefined), undefined); + assert.equal(Selectors.getChannelModerations(state, 'undefined'), undefined); +}); diff --git a/app/mm-redux/selectors/entities/channels.ts b/app/mm-redux/selectors/entities/channels.ts new file mode 100644 index 000000000..e9270fe14 --- /dev/null +++ b/app/mm-redux/selectors/entities/channels.ts @@ -0,0 +1,946 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {General, Permissions} from '../../constants'; +import {getCurrentChannelId, getCurrentUser, getUsers, getMyChannelMemberships, getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/common'; +import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import {getFavoritesPreferences, getMyPreferences, getTeammateNameDisplaySetting, getVisibleTeammate, getVisibleGroupIds} from '@mm-redux/selectors/entities/preferences'; +import {getLastPostPerChannel, getAllPosts} from '@mm-redux/selectors/entities/posts'; +import {getCurrentTeamId, getCurrentTeamMembership, getMyTeams, getTeamMemberships} from '@mm-redux/selectors/entities/teams'; +import {haveICurrentChannelPermission, haveIChannelPermission, haveITeamPermission} from '@mm-redux/selectors/entities/roles'; +import {isCurrentUserSystemAdmin, getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {buildDisplayableChannelListWithUnreadSection, canManageMembersOldPermissions, completeDirectChannelInfo, completeDirectChannelDisplayName, getUserIdFromChannelName, getChannelByName as getChannelByNameHelper, isChannelMuted, getDirectChannelName, isAutoClosed, isDirectChannelVisible, isGroupChannelVisible, isGroupOrDirectChannelVisible, sortChannelsByDisplayName, isFavoriteChannel, isDefault, sortChannelsByRecency} from '@mm-redux/utils/channel_utils'; +import {createIdsSelector} from '@mm-redux/utils/helpers'; + +export {getCurrentChannelId, getMyChannelMemberships, getMyCurrentChannelMembership}; +import {GlobalState} from '@mm-redux/types/store'; +import {Channel, ChannelStats, ChannelMembership, ChannelModeration} from '@mm-redux/types/channels'; +import {UsersState, UserProfile} from '@mm-redux/types/users'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {Post} from '@mm-redux/types/posts'; +import {TeamMembership, Team} from '@mm-redux/types/teams'; +import {NameMappedObjects, UserIDMappedObjects, IDMappedObjects, RelationOneToOne, RelationOneToMany} from '@mm-redux/types/utilities'; + +import {getUserIdsInChannels} from './users'; +import {Config} from '@mm-redux/types/config'; +type SortingType = 'recent' | 'alpha'; + +export function getAllChannels(state: GlobalState): IDMappedObjects { + return state.entities.channels.channels; +} + +export function getAllChannelStats(state: GlobalState): RelationOneToOne { + return state.entities.channels.stats; +} + +export function getChannelsInTeam(state: GlobalState): RelationOneToMany { + return state.entities.channels.channelsInTeam; +} + +export const getDirectChannelsSet: (a: GlobalState) => Set = createSelector(getChannelsInTeam, (channelsInTeam: RelationOneToMany): Set => { + return channelsInTeam && new Set(channelsInTeam['']) || new Set(); +}); +export function getChannelMembersInChannels(state: GlobalState): RelationOneToOne> { + return state.entities.channels.membersInChannel; +} + +export const getKnownUsers: (a: GlobalState) => Set = createSelector( + getChannelMembersInChannels, + getCurrentUserId, + (channelsMemberships: RelationOneToOne>, currentUserId: string): Set => { + const knownUsers: Set = new Set([currentUserId]); + for (const membersInChannel of Object.values(channelsMemberships)) { + for (const member of Object.values(membersInChannel)) { + knownUsers.add(member.user_id); + } + } + return knownUsers; + } +); + +function sortChannelsByRecencyOrAlpha(locale: string, lastPosts: RelationOneToOne, sorting: SortingType, a: Channel, b: Channel) { + if (sorting === 'recent') { + return sortChannelsByRecency(lastPosts, a, b); + } + + return sortChannelsByDisplayName(locale, a, b); +} + +// mapAndSortChannelIds sorts channels, primarily by: +// For all sections except unreads: +// a. All other unread channels +// b. Muted channels +// For unreads section: +// a. Non-muted channels with mentions +// b. Muted channels with mentions +// c. Remaining unread channels +// And then secondary by alphabetical ("alpha") or chronological ("recency") order + +export const mapAndSortChannelIds = (channels: Array, currentUser: UserProfile, myMembers: RelationOneToOne, lastPosts: RelationOneToOne, sorting: SortingType, sortMentionsFirst = false): Array => { + const locale = currentUser.locale || General.DEFAULT_LOCALE; + + const mutedChannelIds = channels. + filter((channel) => isChannelMuted(myMembers[channel.id])). + sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)). + map((channel) => channel.id); + + let hasMentionedChannelIds: string[] = []; + if (sortMentionsFirst) { + hasMentionedChannelIds = channels. + filter((channel) => { + const member = myMembers[channel.id]; + return member && member.mention_count > 0 && !isChannelMuted(member); + }). + sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)). + map((channel) => channel.id); + } + + const otherChannelIds = channels. + filter((channel) => { + return !mutedChannelIds.includes(channel.id) && !hasMentionedChannelIds.includes(channel.id); + }). + sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)). + map((channel) => channel.id); + + return sortMentionsFirst ? hasMentionedChannelIds.concat(mutedChannelIds, otherChannelIds) : otherChannelIds.concat(mutedChannelIds); +}; + +export function filterChannels(unreadIds: Array, favoriteIds: Array, channelIds: Array, unreadsAtTop: boolean, favoritesAtTop: boolean): Array { + let channels: Array = channelIds; + + if (unreadsAtTop) { + channels = channels.filter((id) => { + return !unreadIds.includes(id); + }); + } + + if (favoritesAtTop) { + channels = channels.filter((id) => { + return !favoriteIds.includes(id); + }); + } + + return channels; +} + +export function makeGetChannel(): (b: GlobalState, a: { + id: string; +}) => Channel { + return createSelector(getAllChannels, (state: GlobalState, props: {id: string}) => props.id, (state: GlobalState) => state.entities.users, getTeammateNameDisplaySetting, (allChannels, channelId, users, teammateNameDisplay) => { + const channel = allChannels[channelId]; + + if (channel) { + return completeDirectChannelInfo(users, teammateNameDisplay!, channel); + } + + return channel; + }); +} + +export const getChannel: (b: GlobalState, a: string) => Channel = createSelector(getAllChannels, (state: GlobalState, id: string): string => id, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (allChannels: IDMappedObjects, channelId: string, users: UsersState, teammateNameDisplay: string): Channel => { + const channel = allChannels[channelId]; + + if (channel) { + return completeDirectChannelInfo(users, teammateNameDisplay, channel); + } + + return channel; +}); +export const getCurrentChannel: (a: GlobalState) => Channel = createSelector(getAllChannels, getCurrentChannelId, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (allChannels: IDMappedObjects, currentChannelId: string, users: UsersState, teammateNameDisplay: string): Channel => { + const channel = allChannels[currentChannelId]; + + if (channel) { + return completeDirectChannelInfo(users, teammateNameDisplay, channel); + } + + return channel; +}); +export const getMyChannelMember: (b: GlobalState, a: string) => ChannelMembership | undefined | null = createSelector(getMyChannelMemberships, (state: GlobalState, channelId: string): string => channelId, (channelMemberships: RelationOneToOne, channelId: string): ChannelMembership | undefined | null => { + return channelMemberships[channelId] || null; +}); +export const getCurrentChannelStats: (a: GlobalState) => ChannelStats = createSelector(getAllChannelStats, getCurrentChannelId, (allChannelStats: RelationOneToOne, currentChannelId: string): ChannelStats => { + return allChannelStats[currentChannelId]; +}); +export const isCurrentChannelFavorite: (a: GlobalState) => boolean = createSelector(getMyPreferences, getCurrentChannelId, (preferences: { + [x: string]: PreferenceType; +}, channelId: string): boolean => isFavoriteChannel(preferences, channelId)); +export const isCurrentChannelMuted: (a: GlobalState) => boolean = createSelector(getMyCurrentChannelMembership, (membership?: ChannelMembership | null): boolean => { + if (!membership) { + return false; + } + + return isChannelMuted(membership); +}); +export const isCurrentChannelArchived: (a: GlobalState) => boolean = createSelector(getCurrentChannel, (channel: Channel): boolean => channel.delete_at !== 0); +export const isCurrentChannelDefault: (a: GlobalState) => boolean = createSelector(getCurrentChannel, (channel: Channel): boolean => isDefault(channel)); +export function isCurrentChannelReadOnly(state: GlobalState): boolean { + return isChannelReadOnly(state, getCurrentChannel(state)); +} + +export function isChannelReadOnlyById(state: GlobalState, channelId: string): boolean { + return isChannelReadOnly(state, getChannel(state, channelId)); +} + +export function isChannelReadOnly(state: GlobalState, channel: Channel): boolean { + return channel && channel.name === General.DEFAULT_CHANNEL && !isCurrentUserSystemAdmin(state) && getConfig(state).ExperimentalTownSquareIsReadOnly === 'true'; +} + +export function shouldHideDefaultChannel(state: GlobalState, channel: Channel): boolean { + return channel && channel.name === General.DEFAULT_CHANNEL && !isCurrentUserSystemAdmin(state) && getConfig(state).ExperimentalHideTownSquareinLHS === 'true'; +} +export const countCurrentChannelUnreadMessages: (a: GlobalState) => number = createSelector(getCurrentChannel, getMyCurrentChannelMembership, (channel: Channel, membership?: ChannelMembership | null): number => { + if (!membership) { + return 0; + } + return channel.total_msg_count - membership.msg_count; +}); + +export function getChannelByName(state: GlobalState, channelName: string): Channel | undefined | null { + return getChannelByNameHelper(getAllChannels(state), channelName); +} + +export const getChannelSetInCurrentTeam: (a: GlobalState) => Array = createSelector(getCurrentTeamId, getChannelsInTeam, (currentTeamId: string, channelsInTeam: RelationOneToMany): Array => { + return channelsInTeam && channelsInTeam[currentTeamId] || []; +}); + +function sortAndInjectChannels(channels: IDMappedObjects, channelSet: Array, locale: string): Array { + const currentChannels: Channel[] = []; + + if (typeof channelSet === 'undefined') { + return currentChannels; + } + + channelSet.forEach((c) => { + currentChannels.push(channels[c]); + }); + return currentChannels.sort(sortChannelsByDisplayName.bind(null, locale)); +} + +export const getChannelsInCurrentTeam: (a: GlobalState) => Array = createSelector( + getAllChannels, + getChannelSetInCurrentTeam, + getCurrentUser, + (channels: IDMappedObjects, currentTeamChannelSet: Array, currentUser: UserProfile): Array => { + let locale = General.DEFAULT_LOCALE; + + if (currentUser && currentUser.locale) { + locale = currentUser.locale; + } + + return sortAndInjectChannels(channels, currentTeamChannelSet, locale); + } +); + +export const getChannelsNameMapInTeam: (b: GlobalState, a: string) => NameMappedObjects = createSelector(getAllChannels, getChannelsInTeam, (state: GlobalState, teamId: string): string => teamId, (channels: IDMappedObjects, channelsInTeams: RelationOneToMany, teamId: string): NameMappedObjects => { + const channelsInTeam = channelsInTeams[teamId] || []; + const channelMap: NameMappedObjects = {}; + channelsInTeam.forEach((id) => { + const channel = channels[id]; + channelMap[channel.name] = channel; + }); + return channelMap; +}); + +export const getChannelsNameMapInCurrentTeam: (a: GlobalState) => NameMappedObjects = createSelector(getAllChannels, getChannelSetInCurrentTeam, (channels: IDMappedObjects, currentTeamChannelSet: Array): NameMappedObjects => { + const channelMap: NameMappedObjects = {}; + currentTeamChannelSet.forEach((id) => { + const channel = channels[id]; + channelMap[channel.name] = channel; + }); + return channelMap; +}); + +// Returns both DMs and GMs +export const getAllDirectChannels: (a: GlobalState) => Array = createSelector(getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects, channelSet: Set, users: UsersState, teammateNameDisplay: string): Array => { + const dmChannels: Channel[] = []; + channelSet.forEach((c) => { + dmChannels.push(completeDirectChannelInfo(users, teammateNameDisplay, channels[c])); + }); + return dmChannels; +}); + +// Returns only GMs +export const getGroupChannels: (a: GlobalState) => Array = createSelector(getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects, channelSet: Set, users: UsersState, teammateNameDisplay: string): Array => { + const gmChannels: Channel[] = []; + channelSet.forEach((id) => { + const channel = channels[id]; + + if (channel.type === General.GM_CHANNEL) { + gmChannels.push(completeDirectChannelInfo(users, teammateNameDisplay, channel)); + } + }); + return gmChannels; +}); + +export const getMyChannels: (a: GlobalState) => Array = createSelector( + getChannelsInCurrentTeam, + getAllDirectChannels, + getMyChannelMemberships, + (channels: Array, directChannels: Array, myMembers: RelationOneToOne): Array => { + return [...channels, ...directChannels].filter((c) => myMembers.hasOwnProperty(c.id)); + } +); + +export const getOtherChannels: (b: GlobalState, a?: boolean | null) => Array = createSelector(getChannelsInCurrentTeam, getMyChannelMemberships, (state: GlobalState, archived: boolean | undefined | null = true) => archived, (channels: Array, myMembers: RelationOneToOne, archived?: boolean | null): Array => { + return channels.filter((c) => !myMembers.hasOwnProperty(c.id) && c.type === General.OPEN_CHANNEL && (archived ? true : c.delete_at === 0)); +}); + +export const getArchivedChannels: (a: GlobalState) => Array = createSelector(getChannelsInCurrentTeam, getMyChannelMemberships, (channels: Array, myMembers: RelationOneToOne): Array => { + return channels.filter((c) => myMembers.hasOwnProperty(c.id) && c.delete_at !== 0); +}); + +export const getChannelsWithUnreadSection: (a: GlobalState) => { + unreadChannels: Array; + favoriteChannels: Array; + publicChannels: Array; + privateChannels: Array; + directAndGroupChannels: Array; +} = createSelector( + getCurrentChannelId, + getMyChannels, + getMyChannelMemberships, + getConfig, + getMyPreferences, + getTeammateNameDisplaySetting, + (state: GlobalState): UsersState => state.entities.users, + getLastPostPerChannel, + ( + currentChannelId: string, + channels: Array, + myMembers: RelationOneToOne, + config: Config, + myPreferences: { + [x: string]: PreferenceType; + }, + teammateNameDisplay: string, + usersState: UsersState, + lastPosts: RelationOneToOne + ) => { + const allChannels = channels.map((c) => { + const channel = {...c, + }; + channel.isCurrent = c.id === currentChannelId; + return channel; + }); + return buildDisplayableChannelListWithUnreadSection(usersState, allChannels, myMembers, config, myPreferences, teammateNameDisplay, lastPosts); + } +); + +export const getDefaultChannel: (a: GlobalState) => Channel | undefined | null = createSelector(getAllChannels, getCurrentTeamId, (channels: IDMappedObjects, teamId: string): Channel | undefined | null => { + return Object.keys(channels).map((key) => channels[key]).find((c) => c && c.team_id === teamId && c.name === General.DEFAULT_CHANNEL); +}); + +export const getMembersInCurrentChannel: (a: GlobalState) => UserIDMappedObjects = createSelector(getCurrentChannelId, getChannelMembersInChannels, (currentChannelId: string, members: RelationOneToOne>): UserIDMappedObjects => { + return members[currentChannelId]; +}); + +export const getUnreads: (a: GlobalState) => { + messageCount: number; + mentionCount: number; +} = createSelector( + getAllChannels, + getMyChannelMemberships, + getUsers, + getCurrentUserId, + getCurrentTeamId, + getMyTeams, + getTeamMemberships, + (channels: IDMappedObjects, myMembers: RelationOneToOne, users: IDMappedObjects, currentUserId: string, currentTeamId: string, myTeams: Array, myTeamMemberships: RelationOneToOne): { + messageCount: number; + mentionCount: number; + } => { + let messageCountForCurrentTeam = 0; // Includes message count from channels of current team plus all GM'S and all DM's across teams + let mentionCountForCurrentTeam = 0; // Includes mention count from channels of current team plus all GM'S and all DM's across teams + + Object.keys(myMembers).forEach((channelId) => { + const channel = channels[channelId]; + const m = myMembers[channelId]; + + if (!channel || !m) { + return; + } + + if (channel.team_id !== currentTeamId && channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL) { + return; + } + + let otherUserId = ''; + + if (channel.type === General.DM_CHANNEL) { + otherUserId = getUserIdFromChannelName(currentUserId, channel.name); + + if (users[otherUserId] && users[otherUserId].delete_at === 0) { + mentionCountForCurrentTeam += m.mention_count; + } + } else if (m.mention_count > 0 && channel.delete_at === 0) { + mentionCountForCurrentTeam += m.mention_count; + } + + if (m.notify_props && m.notify_props.mark_unread !== 'mention' && channel.total_msg_count - m.msg_count > 0) { + if (channel.type === General.DM_CHANNEL) { + // otherUserId is guaranteed to have been set above + if (users[otherUserId] && users[otherUserId].delete_at === 0) { + messageCountForCurrentTeam += 1; + } + } else if (channel.delete_at === 0) { + messageCountForCurrentTeam += 1; + } + } + }); + + // Includes mention count and message count from teams other than the current team + // This count does not include GM's and DM's + const otherTeamsUnreadCountForChannels = myTeams.reduce((acc, team) => { + if (currentTeamId !== team.id) { + const member = myTeamMemberships[team.id]; + acc.messageCount += member.msg_count; + acc.mentionCount += member.mention_count; + } + + return acc; + }, { + messageCount: 0, + mentionCount: 0, + }); + + // messageCount is the number of unread channels, mention count is the total number of mentions + return { + messageCount: messageCountForCurrentTeam + otherTeamsUnreadCountForChannels.messageCount, + mentionCount: mentionCountForCurrentTeam + otherTeamsUnreadCountForChannels.mentionCount, + }; + } +); + +export const getUnreadsInCurrentTeam: (a: GlobalState) => { + messageCount: number; + mentionCount: number; +} = createSelector(getCurrentChannelId, getMyChannels, getMyChannelMemberships, getUsers, getCurrentUserId, (currentChannelId: string, channels: Array, myMembers: RelationOneToOne, users: IDMappedObjects, currentUserId: string): { + messageCount: number; + mentionCount: number; +} => { + let messageCount = 0; + let mentionCount = 0; + channels.forEach((channel) => { + const m = myMembers[channel.id]; + + if (m && channel.id !== currentChannelId) { + let otherUserId = ''; + + if (channel.type === 'D') { + otherUserId = getUserIdFromChannelName(currentUserId, channel.name); + + if (users[otherUserId] && users[otherUserId].delete_at === 0) { + mentionCount += channel.total_msg_count - m.msg_count; + } + } else if (m.mention_count > 0 && channel.delete_at === 0) { + mentionCount += m.mention_count; + } + + if (m.notify_props && m.notify_props.mark_unread !== 'mention' && channel.total_msg_count - m.msg_count > 0) { + if (channel.type === 'D') { + if (users[otherUserId] && users[otherUserId].delete_at === 0) { + messageCount += 1; + } + } else if (channel.delete_at === 0) { + messageCount += 1; + } + } + } + }); + return { + messageCount, + mentionCount, + }; +}); +export const canManageChannelMembers: (a: GlobalState) => boolean = createSelector(getCurrentChannel, getCurrentUser, getCurrentTeamMembership, getMyCurrentChannelMembership, getConfig, getLicense, hasNewPermissions, (state: GlobalState): boolean => haveICurrentChannelPermission(state, { + permission: Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS, +}), (state: GlobalState): boolean => haveICurrentChannelPermission(state, { + permission: Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS, +}), (channel: Channel, user: UserProfile, teamMembership: TeamMembership, channelMembership: ChannelMembership | undefined | null, config: Config, license: any, newPermissions: boolean, managePrivateMembers: boolean, managePublicMembers: boolean): boolean => { + if (!channel) { + return false; + } + + if (channel.delete_at !== 0) { + return false; + } + + if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL || channel.name === General.DEFAULT_CHANNEL) { + return false; + } + + if (newPermissions) { + if (channel.type === General.OPEN_CHANNEL) { + return managePublicMembers; + } else if (channel.type === General.PRIVATE_CHANNEL) { + return managePrivateMembers; + } + + return true; + } + + if (!channelMembership) { + return false; + } + + return canManageMembersOldPermissions(channel, user, teamMembership, channelMembership, config, license); +}); // Determine if the user has permissions to manage members in at least one channel of the current team + +export const canManageAnyChannelMembersInCurrentTeam: (a: GlobalState) => boolean = createSelector(getMyChannelMemberships, getCurrentTeamId, (state: GlobalState): GlobalState => state, (members: RelationOneToOne, currentTeamId: string, state: GlobalState): boolean => { + for (const channelId of Object.keys(members)) { + const channel = getChannel(state, channelId); + + if (!channel || channel.team_id !== currentTeamId) { + continue; + } + + if (channel.type === General.OPEN_CHANNEL && haveIChannelPermission(state, { + permission: Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS, + channel: channelId, + team: currentTeamId, + })) { + return true; + } else if (channel.type === General.PRIVATE_CHANNEL && haveIChannelPermission(state, { + permission: Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS, + channel: channelId, + team: currentTeamId, + })) { + return true; + } + } + + return false; +}); +export const getAllDirectChannelIds: (a: GlobalState) => Array = createIdsSelector(getDirectChannelsSet, (directIds: Set): Array => { + return Array.from(directIds); +}); +export const getChannelIdsInCurrentTeam: (a: GlobalState) => Array = createIdsSelector(getCurrentTeamId, getChannelsInTeam, (currentTeamId: string, channelsInTeam: RelationOneToMany): Array => { + return Array.from(channelsInTeam[currentTeamId] || []); +}); +export const getChannelIdsForCurrentTeam: (a: GlobalState) => Array = createIdsSelector(getChannelIdsInCurrentTeam, getAllDirectChannelIds, (channels, direct) => { + return [...channels, ...direct]; +}); +export const getUnreadChannelIds: (b: GlobalState, a?: Channel | null) => Array = createIdsSelector(getAllChannels, getMyChannelMemberships, getChannelIdsForCurrentTeam, (state: GlobalState, lastUnreadChannel: Channel | undefined | null = null): Channel | undefined | null => lastUnreadChannel, (channels: IDMappedObjects, members: RelationOneToOne, teamChannelIds: Array, lastUnreadChannel?: Channel | null): Array => { + const unreadIds = teamChannelIds.filter((id) => { + const c = channels[id]; + const m = members[id]; + + if (c && m) { + const chHasUnread = c.total_msg_count - m.msg_count > 0; + const chHasMention = m.mention_count > 0; + + if (m.notify_props && m.notify_props.mark_unread !== 'mention' && chHasUnread || chHasMention) { + return true; + } + } + + return false; + }); + + if (lastUnreadChannel && !unreadIds.includes(lastUnreadChannel.id)) { + unreadIds.push(lastUnreadChannel.id); + } + + return unreadIds; +}); +export const getUnreadChannels: (b: GlobalState, a?: Channel | null) => Array = createIdsSelector(getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getUnreadChannelIds, getTeammateNameDisplaySetting, (currentUser, profiles, userIdsInChannels: any, channels, unreadIds, settings) => { + // If we receive an unread for a channel and then a mention the channel + // won't be sorted correctly until we receive a message in another channel + if (!currentUser) { + return []; + } + + const allUnreadChannels = unreadIds.filter((id) => channels[id] && channels[id].delete_at === 0).map((id) => { + const c = channels[id]; + + if (c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL) { + return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings!, c); + } + + return c; + }); + return allUnreadChannels; +}); +export const getMapAndSortedUnreadChannelIds: (c: GlobalState, b: Channel, a: SortingType) => Array = createIdsSelector(getUnreadChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, sorting: SortingType = 'alpha') => sorting, (channels, currentUser, myMembers, lastPosts: RelationOneToOne, sorting: SortingType) => { + return mapAndSortChannelIds(channels, currentUser, myMembers, lastPosts, sorting, true); +}); +export const getSortedUnreadChannelIds: (e: GlobalState, d: Channel|null, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => { + return getMapAndSortedUnreadChannelIds(state, lastUnreadChannel, sorting); +}, (unreadChannelIds, mappedAndSortedUnreadChannelIds) => mappedAndSortedUnreadChannelIds); // Favorites + +export const getFavoriteChannels: (a: GlobalState) => Array = createIdsSelector(getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getMyChannelMemberships, getFavoritesPreferences, getChannelIdsForCurrentTeam, getTeammateNameDisplaySetting, getConfig, getMyPreferences, getCurrentChannelId, (currentUser: UserProfile, profiles: IDMappedObjects, userIdsInChannels: any, channels: IDMappedObjects, myMembers: RelationOneToOne, favoriteIds: Array, teamChannelIds: Array, settings: string, config: Config, prefs: { + [x: string]: PreferenceType; +}, currentChannelId: string): Array => { + if (!currentUser) { + return []; + } + + const favoriteChannel = favoriteIds.filter((id) => { + if (!myMembers[id] || !channels[id]) { + return false; + } + + const channel = channels[id]; + const otherUserId = getUserIdFromChannelName(currentUser.id, channel.name); + + if (channel.delete_at !== 0 && channel.id !== currentChannelId) { + return false; + } + + // Deleted users from CLI will not have a profiles entry + + if (channel.type === General.DM_CHANNEL && !profiles[otherUserId]) { + return false; + } + + if (channel.type === General.DM_CHANNEL && !isDirectChannelVisible(profiles[otherUserId] || otherUserId, config, prefs, channel, null, false, currentChannelId)) { + return false; + } else if (channel.type === General.GM_CHANNEL && !isGroupChannelVisible(config, prefs, channel)) { + return false; + } + + return teamChannelIds.includes(id); + }).map((id) => { + const c = channels[id]; + + if (c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL) { + return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings, c); + } + + return c; + }); + return favoriteChannel; +}); + +export const getFavoriteChannelIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getFavoriteChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds); + +export const getSortedFavoriteChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => getFavoriteChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (unreadChannelIds, favoritePreferences, favoriteChannelIds, unreadsAtTop) => { + return filterChannels(unreadChannelIds, favoritePreferences, favoriteChannelIds, unreadsAtTop, false); +}); + +// Public Channels +export const getPublicChannels: (a: GlobalState) => Array = createSelector( + getCurrentUser, + getAllChannels, + getMyChannelMemberships, + getChannelIdsForCurrentTeam, + (currentUser, channels, myMembers, teamChannelIds) => { + if (!currentUser) { + return []; + } + + const publicChannels = teamChannelIds.filter((id) => { + if (!myMembers[id]) { + return false; + } + + const channel = channels[id]; + return teamChannelIds.includes(id) && channel.type === General.OPEN_CHANNEL; + }).map((id) => channels[id]); + return publicChannels; + } +); + +export const getPublicChannelIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector( + getPublicChannels, + getCurrentUser, + getMyChannelMemberships, + getLastPostPerChannel, + (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, + mapAndSortChannelIds +); + +export const getSortedPublicChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector( + getUnreadChannelIds, + getFavoritesPreferences, + (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getPublicChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, + (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, + filterChannels +); + +// Private Channels + +export const getPrivateChannels: (a: GlobalState) => Array = createSelector(getCurrentUser, getAllChannels, getMyChannelMemberships, getChannelIdsForCurrentTeam, (currentUser, channels, myMembers, teamChannelIds) => { + if (!currentUser) { + return []; + } + + const privateChannels = teamChannelIds.filter((id) => { + if (!myMembers[id]) { + return false; + } + + const channel = channels[id]; + return teamChannelIds.includes(id) && channel.type === General.PRIVATE_CHANNEL; + }).map((id) => channels[id]); + return privateChannels; +}); + +export const getPrivateChannelIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getPrivateChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds); + +export const getSortedPrivateChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getPrivateChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, filterChannels); // Direct Messages + +export const getDirectChannels: (a: GlobalState) => Array = createSelector(getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getVisibleTeammate, getVisibleGroupIds, getTeammateNameDisplaySetting, getConfig, getMyPreferences, getLastPostPerChannel, getCurrentChannelId, (currentUser: UserProfile, profiles: IDMappedObjects, userIdsInChannels: any, channels: IDMappedObjects, teammates: Array, groupIds: Array, settings, config, preferences: { + [x: string]: PreferenceType; +}, lastPosts: RelationOneToOne, currentChannelId: string): Array => { + if (!currentUser) { + return []; + } + + const channelValues = Object.keys(channels).map((key) => channels[key]); + const directChannelsIds: string[] = []; + teammates.reduce((result, teammateId) => { + const name = getDirectChannelName(currentUser.id, teammateId); + const channel = channelValues.find((c: Channel) => c && c.name === name); //eslint-disable-line max-nested-callbacks + + if (channel) { + const lastPost = lastPosts[channel.id]; + const otherUser = profiles[getUserIdFromChannelName(currentUser.id, channel.name)]; + + if (!isAutoClosed(config, preferences, channel, lastPost ? lastPost.create_at : 0, otherUser ? otherUser.delete_at : 0, currentChannelId)) { + result.push(channel.id); + } + } + + return result; + }, directChannelsIds); + const directChannels = groupIds.filter((id) => { + const channel = channels[id]; + + if (channel && (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL)) { + const lastPost = lastPosts[channel.id]; + return !isAutoClosed(config, preferences, channels[id], lastPost ? lastPost.create_at : 0, 0, currentChannelId); + } + + return false; + }).concat(directChannelsIds).map((id) => { + const channel = channels[id]; + return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings!, channel); + }); + return directChannels; +}); // getDirectAndGroupChannels returns all direct and group channels, even if they have been manually +// or automatically closed. +// +// This is similar to the getDirectChannels above (which actually also returns group channels, +// but suppresses manually closed group channels but not manually closed direct channels.) This +// method does away with all the suppression, since the webapp client downstream uses this for +// the channel switcher and puts such suppressed channels in a separate category. + +export const getDirectAndGroupChannels: (a: GlobalState) => Array = createSelector(getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getTeammateNameDisplaySetting, (currentUser: UserProfile, profiles: IDMappedObjects, userIdsInChannels: any, channels: IDMappedObjects, settings): Array => { + if (!currentUser) { + return []; + } + + return Object.keys(channels).map((key) => channels[key]).filter((channel: Channel): boolean => Boolean(channel)).filter((channel: Channel): boolean => channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL).map((channel: Channel): Channel => completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[channel.id], settings!, channel)); +}); + +export const getDirectChannelIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getDirectChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds); + +export const getSortedDirectChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getDirectChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, filterChannels); + +export function getGroupOrDirectChannelVisibility(state: GlobalState, channelId: string): boolean { + return isGroupOrDirectChannelVisible(getChannel(state, channelId), getMyChannelMemberships(state), getConfig(state), getMyPreferences(state), getCurrentUser(state).id, getUsers(state), getLastPostPerChannel(state)); +} + +// Filters post IDs by the given condition. +// The condition function receives as parameters the associated channel object and the post object. + +export const filterPostIds = (condition: (b: Channel, a: Post) => boolean) => { + if (typeof condition !== 'function') { + throw new TypeError(`${condition} is not a function`); + } + + return (createSelector(getAllChannels, getAllPosts, (state: GlobalState, postIds: Array): Array => postIds, (channels: IDMappedObjects, posts: IDMappedObjects, postIds: Array): Array => { + return postIds.filter((postId) => { + const post = posts[postId]; + let channel; + + if (post) { + channel = channels[post.channel_id]; + } + + return post && channel && condition(channel, post); + }); + }) as (b: GlobalState, a: Array) => Array); +}; + +const getProfiles = (currentUserId: string, usersIdsInChannel: Array, users: IDMappedObjects): Array => { + const profiles: UserProfile[] = []; + usersIdsInChannel.forEach((userId) => { + if (userId !== currentUserId) { + profiles.push(users[userId]); + } + }); + return profiles; +}; + +export const getChannelsWithUserProfiles: (a: GlobalState) => Array<{ + profiles: Array; +} & Channel> = createSelector(getUserIdsInChannels, getUsers, getGroupChannels, getCurrentUserId, (channelUserMap: RelationOneToMany, users: IDMappedObjects, channels: Array, currentUserId: string) => { + return channels.map((channel: Channel): { + profiles: Array; + } & Channel => { + const profiles = getProfiles(currentUserId, channelUserMap[channel.id] || [], users); + return {...channel, + profiles, + }; + }); +}); +const getAllActiveChannels = createSelector(getPublicChannels, getPrivateChannels, getDirectChannels, (publicChannels, privateChannels, directChannels) => { + const allChannels = [...publicChannels, ...privateChannels, ...directChannels]; + return allChannels; +}); +export const getAllChannelIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getAllActiveChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds); + +export const getAllSortedChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getAllChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, filterChannels); +let lastChannels: {type: string;name: string;items: string[]}[]; + +const hasChannelsChanged = (channels: {type: string;name: string;items: string[]}[]) => { + if (!lastChannels || lastChannels.length !== channels.length) { + return true; + } + + for (let i = 0; i < channels.length; i++) { + if (channels[i].type !== lastChannels[i].type || channels[i].items !== lastChannels[i].items) { + return true; + } + } + + return false; +}; + +export const getOrderedChannelIds = (state: GlobalState, lastUnreadChannel: Channel|null, grouping: 'by_type' | 'none', sorting: SortingType, unreadsAtTop: boolean, favoritesAtTop: boolean) => { + const channels: {type: string;name: string;items: string[]}[] = []; + + if (grouping === 'by_type') { + channels.push({ + type: 'public', + name: 'PUBLIC CHANNELS', + items: getSortedPublicChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + }); + channels.push({ + type: 'private', + name: 'PRIVATE CHANNELS', + items: getSortedPrivateChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + }); + channels.push({ + type: 'direct', + name: 'DIRECT MESSAGES', + items: getSortedDirectChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + }); + } else { + // Combine all channel types + let type = 'alpha'; + let name = 'CHANNELS'; + + if (sorting === 'recent') { + type = 'recent'; + name = 'RECENT ACTIVITY'; + } + + channels.push({ + type, + name, + items: getAllSortedChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + }); + } + + if (favoritesAtTop) { + channels.unshift({ + type: 'favorite', + name: 'FAVORITE CHANNELS', + items: getSortedFavoriteChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + }); + } + + if (unreadsAtTop) { + channels.unshift({ + type: 'unreads', + name: 'UNREADS', + items: getSortedUnreadChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), + }); + } + + if (hasChannelsChanged(channels)) { + lastChannels = channels; + } + + return lastChannels; +}; + +// Added for backwards compatibility +// Can be removed once webapp includes new sidebar preferences + +export const getSortedPublicChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, getPublicChannelIds, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritePreferences, publicChannelIds, favoritesAtTop) => { + return filterChannels(unreadChannelIds, favoritePreferences, publicChannelIds, false, favoritesAtTop); +}); + +export const getSortedPrivateChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, getPrivateChannelIds, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritePreferences, privateChannelId, favoritesAtTop) => { + return filterChannels(unreadChannelIds, favoritePreferences, privateChannelId, false, favoritesAtTop); +}); + +export const getSortedFavoriteChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoriteChannelIds, (unreadChannelIds, favoriteChannelIds) => favoriteChannelIds); + +export const getSortedDirectChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, getDirectChannelIds, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritePreferences, directChannelIds, favoritesAtTop) => { + return filterChannels(unreadChannelIds, favoritePreferences, directChannelIds, false, favoritesAtTop); +}); + +export const getDefaultChannelForTeams: (a: GlobalState) => RelationOneToOne = createSelector(getAllChannels, (channels: IDMappedObjects): RelationOneToOne => { + const result: RelationOneToOne = {}; + + for (const channel of Object.keys(channels).map((key) => channels[key])) { + if (channel && channel.name === General.DEFAULT_CHANNEL) { + result[channel.team_id] = channel; + } + } + + return result; +}); + +export const getMyFirstChannelForTeams: (a: GlobalState) => RelationOneToOne = createSelector(getAllChannels, getMyChannelMemberships, getMyTeams, getCurrentUser, (allChannels: IDMappedObjects, myChannelMemberships: RelationOneToOne, myTeams: Array, currentUser: UserProfile): RelationOneToOne => { + const locale = currentUser.locale || General.DEFAULT_LOCALE; + const result: RelationOneToOne = {}; + + for (const team of myTeams) { + // Get a sorted array of all channels in the team that the current user is a member of + const teamChannels = Object.values(allChannels).filter((channel: Channel) => channel && channel.team_id === team.id && Boolean(myChannelMemberships[channel.id])).sort(sortChannelsByDisplayName.bind(null, locale)); + + if (teamChannels.length === 0) { + continue; + } + + result[team.id] = teamChannels[0]; + } + + return result; +}); +export const getRedirectChannelNameForTeam = (state: GlobalState, teamId: string): string => { + const defaultChannelForTeam = getDefaultChannelForTeams(state)[teamId]; + const myFirstChannelForTeam = getMyFirstChannelForTeams(state)[teamId]; + const canIJoinPublicChannelsInTeam = !hasNewPermissions(state) || haveITeamPermission(state, { + team: teamId, + permission: Permissions.JOIN_PUBLIC_CHANNELS, + }); + const myChannelMemberships = getMyChannelMemberships(state); + const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannelForTeam && myChannelMemberships[defaultChannelForTeam.id]); + + if (iAmMemberOfTheTeamDefaultChannel || canIJoinPublicChannelsInTeam) { + return General.DEFAULT_CHANNEL; + } + + return myFirstChannelForTeam && myFirstChannelForTeam.name || General.DEFAULT_CHANNEL; +}; + +// isManually unread looks into state if the provided channelId is marked as unread by the user. +export function isManuallyUnread(state: GlobalState, channelId?: string): boolean { + if (!channelId) { + return false; + } + + return Boolean(state.entities.channels.manuallyUnread[channelId]); +} + +export function getChannelModerations(state: GlobalState, channelId: string): Array { + return state.entities.channels.channelModerations[channelId]; +} diff --git a/app/mm-redux/selectors/entities/common.ts b/app/mm-redux/selectors/entities/common.ts new file mode 100644 index 000000000..0846bc2de --- /dev/null +++ b/app/mm-redux/selectors/entities/common.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {GlobalState} from '@mm-redux/types/store'; +import {UserProfile} from '@mm-redux/types/users'; +import {ChannelMembership, Channel} from '@mm-redux/types/channels'; +import {RelationOneToOne, IDMappedObjects} from '@mm-redux/types/utilities'; + +import {createSelector} from 'reselect'; + +// Channels + +export function getCurrentChannelId(state: GlobalState): string { + return state.entities.channels.currentChannelId; +} + +export function getMyChannelMemberships(state: GlobalState): RelationOneToOne { + return state.entities.channels.myMembers; +} + +export const getMyCurrentChannelMembership: (a: GlobalState) => ChannelMembership | undefined | null = createSelector(getCurrentChannelId, getMyChannelMemberships, (currentChannelId, channelMemberships) => { + return channelMemberships[currentChannelId] || null; +}); + +// Users + +export function getCurrentUser(state: GlobalState): UserProfile { + return state.entities.users.profiles[getCurrentUserId(state)]; +} + +export function getCurrentUserId(state: GlobalState): string { + return state.entities.users.currentUserId; +} + +export function getUsers(state: GlobalState): IDMappedObjects { + return state.entities.users.profiles; +} diff --git a/app/mm-redux/selectors/entities/emojis.test.js b/app/mm-redux/selectors/entities/emojis.test.js new file mode 100644 index 000000000..35d7b1109 --- /dev/null +++ b/app/mm-redux/selectors/entities/emojis.test.js @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import TestHelper from 'test/test_helper'; +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; + +import {getCustomEmojiIdsSortedByName} from '@mm-redux/selectors/entities/emojis'; + +describe('Selectors.Integrations', () => { + TestHelper.initBasic(); + + const emoji1 = {id: TestHelper.generateId(), name: 'a', creator_id: TestHelper.generateId()}; + const emoji2 = {id: TestHelper.generateId(), name: 'b', creator_id: TestHelper.generateId()}; + const emoji3 = {id: TestHelper.generateId(), name: '0', creator_id: TestHelper.generateId()}; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + emojis: { + customEmoji: { + [emoji1.id]: emoji1, + [emoji2.id]: emoji2, + [emoji3.id]: emoji3, + }, + }, + }, + }); + + it('should get sorted emoji ids', () => { + assert.deepEqual(getCustomEmojiIdsSortedByName(testState), [emoji3.id, emoji1.id, emoji2.id]); + }); +}); diff --git a/app/mm-redux/selectors/entities/emojis.ts b/app/mm-redux/selectors/entities/emojis.ts new file mode 100644 index 000000000..2fdc1caf6 --- /dev/null +++ b/app/mm-redux/selectors/entities/emojis.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {createIdsSelector} from '@mm-redux/utils/helpers'; +import {GlobalState} from '@mm-redux/types/store'; +import {CustomEmoji} from '@mm-redux/types/emojis'; +import {IDMappedObjects} from '@mm-redux/types/utilities'; +export function getCustomEmojis(state: GlobalState): IDMappedObjects { + if (state.entities.general.config.EnableCustomEmoji !== 'true') { + return {}; + } + + return state.entities.emojis.customEmoji; +} + +export const getCustomEmojisAsMap: (state: GlobalState) => Map = createSelector( + getCustomEmojis, + (emojis) => { + const map = new Map(); + Object.keys(emojis).forEach((key: string) => { + map.set(key, emojis[key]); + }); + return map; + } +); + +export const getCustomEmojisByName: (state: GlobalState) => Map = createSelector( + getCustomEmojis, + (emojis: IDMappedObjects): Map => { + const map: Map = new Map(); + + Object.keys(emojis).forEach((key: string) => { + map.set(emojis[key].name, emojis[key]); + }); + + return map; + } +); + +export const getCustomEmojiIdsSortedByName: (state: GlobalState) => Array = createIdsSelector( + (state) => state.entities.emojis.customEmoji, + (emojis: IDMappedObjects): Array => { + return Object.keys(emojis).sort( + (a: string, b: string): number => emojis[a].name.localeCompare(emojis[b].name) + ); + } +); diff --git a/app/mm-redux/selectors/entities/files.ts b/app/mm-redux/selectors/entities/files.ts new file mode 100644 index 000000000..a5fbeaa57 --- /dev/null +++ b/app/mm-redux/selectors/entities/files.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as reselect from 'reselect'; + +import {getCurrentUserLocale} from '@mm-redux/selectors/entities/i18n'; + +import {sortFileInfos} from '@mm-redux/utils/file_utils'; + +import * as types from '@mm-redux/types'; +import {GlobalState} from '@mm-redux/types/store'; + +function getAllFiles(state: types.store.GlobalState) { + return state.entities.files.files; +} + +function getFilesIdsForPost(state: types.store.GlobalState, postId: string) { + if (postId) { + return state.entities.files.fileIdsByPostId[postId] || []; + } + + return []; +} + +export function getFilePublicLink(state: GlobalState) { + return state.entities.files.filePublicLink; +} + +export function makeGetFilesForPost() { + return reselect.createSelector( + [getAllFiles, getFilesIdsForPost, getCurrentUserLocale], + (allFiles, fileIdsForPost, locale) => { + const fileInfos = fileIdsForPost.map((id) => allFiles[id]).filter((id) => Boolean(id)); + + return sortFileInfos(fileInfos, locale); + } + ); +} diff --git a/app/mm-redux/selectors/entities/general.test.js b/app/mm-redux/selectors/entities/general.test.js new file mode 100644 index 000000000..6e1df4e22 --- /dev/null +++ b/app/mm-redux/selectors/entities/general.test.js @@ -0,0 +1,343 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {General} from '../../constants'; + +import * as Selectors from '@mm-redux/selectors/entities/general'; + +describe('Selectors.General', () => { + it('canUploadFilesOnMobile', () => { + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), true); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), false); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'true', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), true); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableMobileFileUpload: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), false); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'false', + EnableMobileFileUpload: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), false); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'true', + EnableMobileFileUpload: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), false); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableMobileFileUpload: 'true', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), true); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'false', + EnableMobileFileUpload: 'true', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), false); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'true', + EnableMobileFileUpload: 'true', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), true); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'false', + }, + license: { + IsLicensed: 'false', + Compliance: 'false', + }, + }, + }, + }), false); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'true', + EnableMobileFileUpload: 'false', + }, + license: { + IsLicensed: 'false', + Compliance: 'false', + }, + }, + }, + }), true); + + assert.equal(Selectors.canUploadFilesOnMobile({ + entities: { + general: { + config: { + EnableFileAttachments: 'true', + EnableMobileFileUpload: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'false', + }, + }, + }, + }), true); + }); + + it('canDownloadFilesOnMobile', () => { + assert.equal(Selectors.canDownloadFilesOnMobile({ + entities: { + general: { + config: { + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), true); + + assert.equal(Selectors.canDownloadFilesOnMobile({ + entities: { + general: { + config: { + EnableMobileFileDownload: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'true,', + }, + }, + }, + }), false); + + assert.equal(Selectors.canDownloadFilesOnMobile({ + entities: { + general: { + config: { + EnableMobileFileDownload: 'true', + }, + license: { + IsLicensed: 'true', + Compliance: 'true', + }, + }, + }, + }), true); + + assert.equal(Selectors.canDownloadFilesOnMobile({ + entities: { + general: { + config: { + EnableMobileFileDownload: 'false', + }, + license: { + IsLicensed: 'false', + Compliance: 'false', + }, + }, + }, + }), true); + + assert.equal(Selectors.canDownloadFilesOnMobile({ + entities: { + general: { + config: { + EnableMobileFileDownload: 'false', + }, + license: { + IsLicensed: 'true', + Compliance: 'false', + }, + }, + }, + }), true); + }); + + it('hasNewPermissions', () => { + const state = { + entities: { + general: { + serverVersion: '4.8.0', + }, + }, + }; + assert.equal(Selectors.hasNewPermissions(state), false); + state.entities.general.serverVersion = '4.8.0.dev.123123'; + assert.equal(Selectors.hasNewPermissions(state), true); + state.entities.general.serverVersion = '4.8.0.4.8.1.ffffff'; + assert.equal(Selectors.hasNewPermissions(state), false); + state.entities.general.serverVersion = '4.8.0.3607.2f31498e967dc08ed38d7a2d7a306825.true'; + assert.equal(Selectors.hasNewPermissions(state), true); + state.entities.general.serverVersion = '4.8.1.3607.2f31498e967dc08ed38d7a2d7a306825.true'; + assert.equal(Selectors.hasNewPermissions(state), true); + state.entities.general.serverVersion = '4.9.0'; + assert.equal(Selectors.hasNewPermissions(state), true); + state.entities.general.serverVersion = '4.10.0'; + assert.equal(Selectors.hasNewPermissions(state), true); + state.entities.general.serverVersion = '5.10.0.dev'; + assert.equal(Selectors.hasNewPermissions(state), true); + }); + + describe('getAutolinkedUrlSchemes', () => { + it('setting doesn\'t exist', () => { + const state = { + entities: { + general: { + config: { + }, + }, + }, + }; + + assert.deepEqual(Selectors.getAutolinkedUrlSchemes(state), General.DEFAULT_AUTOLINKED_URL_SCHEMES); + assert.equal(Selectors.getAutolinkedUrlSchemes(state), Selectors.getAutolinkedUrlSchemes(state)); + }); + + it('no custom url schemes', () => { + const state = { + entities: { + general: { + config: { + CustomUrlSchemes: '', + }, + }, + }, + }; + + assert.deepEqual(Selectors.getAutolinkedUrlSchemes(state), General.DEFAULT_AUTOLINKED_URL_SCHEMES); + assert.equal(Selectors.getAutolinkedUrlSchemes(state), Selectors.getAutolinkedUrlSchemes(state)); + }); + + it('one custom url scheme', () => { + const state = { + entities: { + general: { + config: { + CustomUrlSchemes: 'dns', + }, + }, + }, + }; + + assert.deepEqual(Selectors.getAutolinkedUrlSchemes(state), [...General.DEFAULT_AUTOLINKED_URL_SCHEMES, 'dns']); + assert.equal(Selectors.getAutolinkedUrlSchemes(state), Selectors.getAutolinkedUrlSchemes(state)); + }); + + it('multiple custom url schemes', () => { + const state = { + entities: { + general: { + config: { + CustomUrlSchemes: 'dns,steam,shttp', + }, + }, + }, + }; + + assert.deepEqual(Selectors.getAutolinkedUrlSchemes(state), [...General.DEFAULT_AUTOLINKED_URL_SCHEMES, 'dns', 'steam', 'shttp']); + assert.equal(Selectors.getAutolinkedUrlSchemes(state), Selectors.getAutolinkedUrlSchemes(state)); + }); + }); +}); + diff --git a/app/mm-redux/selectors/entities/general.ts b/app/mm-redux/selectors/entities/general.ts new file mode 100644 index 000000000..0d2a9a1bf --- /dev/null +++ b/app/mm-redux/selectors/entities/general.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {General} from '../../constants'; +import {GlobalState} from '@mm-redux/types/store'; +import {Config} from '@mm-redux/types/config'; + +export function getConfig(state: GlobalState): Partial { + return state.entities.general.config; +} + +export function getLicense(state: GlobalState): any { + return state.entities.general.license; +} + +export function getSupportedTimezones(state: GlobalState): Array { + return state.entities.general.timezones; +} + +export function getCurrentUrl(state: GlobalState): string { + return state.entities.general.credentials.url; +} + +export function isCompatibleWithJoinViewTeamPermissions(state: GlobalState): boolean { + const version = state.entities.general.serverVersion; + return isMinimumServerVersion(version, 5, 10, 0) || + (version.indexOf('dev') !== -1 && isMinimumServerVersion(version, 5, 8, 0)) || + (version.match(/^5.8.\d.\d\d\d\d.*$/) !== null && isMinimumServerVersion(version, 5, 8, 0)); +} + +export function hasNewPermissions(state: GlobalState): boolean { + const version = state.entities.general.serverVersion; + + // FIXME This must be changed to 4, 9, 0 before we generate the 4.9.0 release + return isMinimumServerVersion(version, 4, 9, 0) || + (version.indexOf('dev') !== -1 && isMinimumServerVersion(version, 4, 8, 0)) || + (version.match(/^4.8.\d.\d\d\d\d.*$/) !== null && isMinimumServerVersion(version, 4, 8, 0)); +} + +export const canUploadFilesOnMobile: (a: GlobalState) => boolean = createSelector( + getConfig, + getLicense, + (config: Config, license: any): boolean => { + // Defaults to true if either setting doesn't exist + return config.EnableFileAttachments !== 'false' && + (license.IsLicensed === 'false' || license.Compliance === 'false' || config.EnableMobileFileUpload !== 'false'); + } +); + +export const canDownloadFilesOnMobile: (a: GlobalState) => boolean = createSelector( + getConfig, + getLicense, + (config: Config, license: any): boolean => { + // Defaults to true if the setting doesn't exist + return license.IsLicensed === 'false' || license.Compliance === 'false' || config.EnableMobileFileDownload !== 'false'; + } +); + +export const getAutolinkedUrlSchemes: (a: GlobalState) => string[] = createSelector( + getConfig, + (config: Config): string[] => { + if (!config.CustomUrlSchemes) { + return General.DEFAULT_AUTOLINKED_URL_SCHEMES; + } + + return [ + ...General.DEFAULT_AUTOLINKED_URL_SCHEMES, + ...config.CustomUrlSchemes.split(','), + ]; + } +); + +export const getServerVersion = (state: GlobalState): string => { + return state.entities.general.serverVersion; +}; diff --git a/app/mm-redux/selectors/entities/groups.test.js b/app/mm-redux/selectors/entities/groups.test.js new file mode 100644 index 000000000..c1f781ce2 --- /dev/null +++ b/app/mm-redux/selectors/entities/groups.test.js @@ -0,0 +1,117 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import * as Selectors from '@mm-redux/selectors/entities/groups'; + +describe('Selectors.Groups', () => { + const teamID = 'c6ubwm63apgftbjs71enbjjpsh'; + const expectedAssociatedGroupID1 = 'xh585kyz3tn55q6ipfo57btwnc'; + const expectedAssociatedGroupID2 = 'emdwu98u6jg9xfn9p5zu48bojo'; + const teamAssociatedGroupIDs = [expectedAssociatedGroupID1, expectedAssociatedGroupID2]; + + const channelID = 'c6ubwm63apgftbjs71enbjjpzz'; + const expectedAssociatedGroupID3 = 'xos794c6tfb57eog481acokozc'; + const expectedAssociatedGroupID4 = 'tnd8zod9f3fdtqosxjmhwucbth'; + const channelAssociatedGroupIDs = [expectedAssociatedGroupID3, expectedAssociatedGroupID4]; + const testState = deepFreezeAndThrowOnMutation({ + entities: { + groups: { + syncables: {}, + members: {}, + groups: { + [expectedAssociatedGroupID1]: { + id: expectedAssociatedGroupID1, + name: '9uobsi3xb3y5tfjb3ze7umnh1o', + display_name: 'abc', + description: '', + source: 'ldap', + remote_id: 'abc', + create_at: 1553808969975, + update_at: 1553808969975, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + [expectedAssociatedGroupID3]: { + id: expectedAssociatedGroupID3, + name: '5mte953ncbfpunpr3zmtopiwbo', + display_name: 'developers', + description: '', + source: 'ldap', + remote_id: 'developers', + create_at: 1553808970570, + update_at: 1553808970570, + delete_at: 0, + has_syncables: false, + member_count: 5, + }, + [expectedAssociatedGroupID4]: { + id: [expectedAssociatedGroupID4], + name: 'nobctj4brfgtpj3a1peiyq47tc', + display_name: 'engineering', + description: '', + source: 'ldap', + create_at: 1553808971099, + remote_id: 'engineering', + update_at: 1553808971099, + delete_at: 0, + has_syncables: false, + member_count: 8, + }, + [expectedAssociatedGroupID2]: { + id: expectedAssociatedGroupID2, + name: '7ybu9oy77jgedqp4pph8f4j5ge', + display_name: 'xyz', + description: '', + source: 'ldap', + remote_id: 'xyz', + create_at: 1553808972099, + update_at: 1553808972099, + delete_at: 0, + has_syncables: false, + member_count: 2, + }, + }, + }, + teams: { + groupsAssociatedToTeam: { + [teamID]: {ids: teamAssociatedGroupIDs}, + }, + }, + channels: { + groupsAssociatedToChannel: { + [channelID]: {ids: channelAssociatedGroupIDs}, + }, + }, + }, + }); + + it('getGroupsAssociatedToTeam', () => { + const expected = [ + testState.entities.groups.groups[expectedAssociatedGroupID1], + testState.entities.groups.groups[expectedAssociatedGroupID2], + ]; + assert.deepEqual(Selectors.getGroupsAssociatedToTeam(testState, teamID), expected); + }); + + it('getGroupsNotAssociatedToTeam', () => { + const expected = Object.entries(testState.entities.groups.groups).filter(([groupID]) => !teamAssociatedGroupIDs.includes(groupID)).map(([, group]) => group); + assert.deepEqual(Selectors.getGroupsNotAssociatedToTeam(testState, teamID), expected); + }); + + it('getGroupsAssociatedToChannel', () => { + const expected = [ + testState.entities.groups.groups[expectedAssociatedGroupID3], + testState.entities.groups.groups[expectedAssociatedGroupID4], + ]; + assert.deepEqual(Selectors.getGroupsAssociatedToChannel(testState, channelID), expected); + }); + + it('getGroupsNotAssociatedToChannel', () => { + const expected = Object.entries(testState.entities.groups.groups).filter(([groupID]) => !channelAssociatedGroupIDs.includes(groupID)).map(([, group]) => group); + assert.deepEqual(Selectors.getGroupsNotAssociatedToChannel(testState, channelID), expected); + }); +}); diff --git a/app/mm-redux/selectors/entities/groups.ts b/app/mm-redux/selectors/entities/groups.ts new file mode 100644 index 000000000..8063d87ca --- /dev/null +++ b/app/mm-redux/selectors/entities/groups.ts @@ -0,0 +1,92 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import * as reselect from 'reselect'; +import {GlobalState} from '@mm-redux/types/store'; +const emptyList: any[] = []; +const emptySyncables = { + teams: [], + channels: [], +}; + +export function getAllGroups(state: GlobalState) { + return state.entities.groups.groups; +} + +export function getGroup(state: GlobalState, id: string) { + return getAllGroups(state)[id]; +} + +export function getGroupMemberCount(state: GlobalState, id: string) { + const memberData = state.entities.groups.members; + const groupMemberData = memberData[id]; + if (!groupMemberData) { + return 0; + } + return memberData[id].totalMemberCount; +} + +function getGroupSyncables(state: GlobalState, id: string) { + return state.entities.groups.syncables[id] || emptySyncables; +} + +export function getGroupTeams(state: GlobalState, id: string) { + return getGroupSyncables(state, id).teams; +} + +export function getGroupChannels(state: GlobalState, id: string) { + return getGroupSyncables(state, id).channels; +} + +export function getGroupMembers(state: GlobalState, id: string) { + const groupMemberData = state.entities.groups.members[id]; + if (!groupMemberData) { + return emptyList; + } + return groupMemberData.members; +} + +const teamGroupIDs = (state: GlobalState, teamID: string) => (state.entities.teams.groupsAssociatedToTeam[teamID] == null ? undefined : state.entities.teams.groupsAssociatedToTeam[teamID].ids == null ? undefined : state.entities.teams.groupsAssociatedToTeam[teamID].ids) || []; + +const channelGroupIDs = (state: GlobalState, channelID: string) => (state.entities.channels.groupsAssociatedToChannel[channelID] == null ? undefined : state.entities.channels.groupsAssociatedToChannel[channelID].ids == null ? undefined : state.entities.channels.groupsAssociatedToChannel[channelID].ids) || []; + +const getTeamGroupIDSet = reselect.createSelector( + teamGroupIDs, + (teamIDs) => new Set(teamIDs), +); + +const getChannelGroupIDSet = reselect.createSelector( + channelGroupIDs, + (channelIDs) => new Set(channelIDs), +); + +export const getGroupsNotAssociatedToTeam = reselect.createSelector( + getAllGroups, + (state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID), + (allGroups, teamGroupIDSet) => { + return Object.entries(allGroups).filter(([groupID]) => !teamGroupIDSet.has(groupID)).map((entry) => entry[1]); + } +); + +export const getGroupsAssociatedToTeam = reselect.createSelector( + getAllGroups, + (state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID), + (allGroups, teamGroupIDSet) => { + return Object.entries(allGroups).filter(([groupID]) => teamGroupIDSet.has(groupID)).map((entry) => entry[1]); + } +); + +export const getGroupsNotAssociatedToChannel = reselect.createSelector( + getAllGroups, + (state: GlobalState, channelID: string) => getChannelGroupIDSet(state, channelID), + (allGroups, channelGroupIDSet) => { + return Object.entries(allGroups).filter(([groupID]) => !channelGroupIDSet.has(groupID)).map((entry) => entry[1]); + } +); + +export const getGroupsAssociatedToChannel = reselect.createSelector( + getAllGroups, + (state: GlobalState, channelID: string) => getChannelGroupIDSet(state, channelID), + (allGroups, channelGroupIDSet) => { + return Object.entries(allGroups).filter(([groupID]) => channelGroupIDSet.has(groupID)).map((entry) => entry[1]); + } +); diff --git a/app/mm-redux/selectors/entities/i18n.test.js b/app/mm-redux/selectors/entities/i18n.test.js new file mode 100644 index 000000000..c5ec4827f --- /dev/null +++ b/app/mm-redux/selectors/entities/i18n.test.js @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import TestHelper from 'test/test_helper'; +import * as Selectors from '@mm-redux/selectors/entities/i18n'; + +describe('Selectors.I18n', () => { + describe('getCurrentUserLocale', () => { + it('not logged in', () => { + const state = { + entities: { + users: { + currentUserId: '', + profiles: {}, + }, + }, + }; + + assert.equal(Selectors.getCurrentUserLocale(state, 'default'), 'default'); + }); + + it('current user not loaded', () => { + const currentUserId = TestHelper.generateId(); + const state = { + entities: { + users: { + currentUserId, + profiles: {}, + }, + }, + }; + + assert.equal(Selectors.getCurrentUserLocale(state, 'default'), 'default'); + }); + + it('current user without locale set', () => { + const currentUserId = TestHelper.generateId(); + const state = { + entities: { + users: { + currentUserId, + profiles: { + [currentUserId]: {locale: ''}, + }, + }, + }, + }; + + assert.equal(Selectors.getCurrentUserLocale(state, 'default'), 'default'); + }); + + it('current user with locale set', () => { + const currentUserId = TestHelper.generateId(); + const state = { + entities: { + users: { + currentUserId, + profiles: { + [currentUserId]: {locale: 'fr'}, + }, + }, + }, + }; + + assert.equal(Selectors.getCurrentUserLocale(state, 'default'), 'fr'); + }); + }); +}); diff --git a/app/mm-redux/selectors/entities/i18n.ts b/app/mm-redux/selectors/entities/i18n.ts new file mode 100644 index 000000000..68a70b811 --- /dev/null +++ b/app/mm-redux/selectors/entities/i18n.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {getCurrentUser} from '@mm-redux/selectors/entities/common'; +import {General} from '../../constants'; +import {GlobalState} from '@mm-redux/types/store'; +export function getCurrentUserLocale(state: GlobalState, defaultLocale = General.DEFAULT_LOCALE) { + const currentUser = getCurrentUser(state); + + if (!currentUser) { + return defaultLocale; + } + + return currentUser.locale || defaultLocale; +} diff --git a/app/mm-redux/selectors/entities/index.ts b/app/mm-redux/selectors/entities/index.ts new file mode 100644 index 000000000..e477d1259 --- /dev/null +++ b/app/mm-redux/selectors/entities/index.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as bots from './bots'; +import * as channels from './channels'; +import * as common from './common'; +import * as emojis from './emojis'; +import * as files from './files'; +import * as general from './general'; +import * as groups from './groups'; +import * as i18n from './i18n'; +import * as integrations from './integrations'; +import * as jobs from './jobs'; +import * as posts from './posts'; +import * as preferences from './preferences'; +import * as roles_helpers from './roles_helpers'; +import * as roles from './roles'; +import * as schemes from './schemes'; +import * as search from './search'; +import * as teams from './teams'; +import * as timezone from './timezone'; +import * as typing from './typing'; +import * as users from './users'; + +export { + bots, + channels, + common, + emojis, + files, + general, + groups, + i18n, + integrations, + jobs, + posts, + preferences, + roles_helpers, + roles, + schemes, + search, + teams, + timezone, + typing, + users, +}; diff --git a/app/mm-redux/selectors/entities/integrations.test.js b/app/mm-redux/selectors/entities/integrations.test.js new file mode 100644 index 000000000..a766856b0 --- /dev/null +++ b/app/mm-redux/selectors/entities/integrations.test.js @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import TestHelper from 'test/test_helper'; +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; + +import {getAllCommands, getAutocompleteCommandsList, getOutgoingHooksInCurrentTeam} from './integrations'; + +describe('Selectors.Integrations', () => { + TestHelper.initBasic(); + + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + + const hook1 = TestHelper.fakeOutgoingHookWithId(team1.id); + const hook2 = TestHelper.fakeOutgoingHookWithId(team1.id); + const hook3 = TestHelper.fakeOutgoingHookWithId(team2.id); + + const hooks = {[hook1.id]: hook1, [hook2.id]: hook2, [hook3.id]: hook3}; + + const command1 = {id: TestHelper.generateId(), ...TestHelper.testCommand(team1.id), auto_complete: false}; + const command2 = {id: TestHelper.generateId(), ...TestHelper.testCommand(team2.id)}; + const command3 = TestHelper.testCommand(team1.id); + const command4 = TestHelper.testCommand(team2.id); + + const commands = {[command1.id]: command1, [command2.id]: command2}; + const systemCommands = {[command3.trigger]: command3, [command4.trigger]: command4}; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + integrations: { + outgoingHooks: hooks, + commands, + systemCommands, + }, + }, + }); + + it('should return outgoing hooks in current team', () => { + const hooksInCurrentTeam1 = [hook1, hook2]; + assert.deepEqual(getOutgoingHooksInCurrentTeam(testState), hooksInCurrentTeam1); + }); + + it('should get all commands', () => { + const commandsInState = {...commands, ...systemCommands}; + assert.deepEqual(getAllCommands(testState), commandsInState); + }); + + it('should get all autocomplete commands by teamId', () => { + const autocompleteCommandsForTeam = [command3]; + assert.deepEqual(getAutocompleteCommandsList(testState), autocompleteCommandsForTeam); + }); +}); diff --git a/app/mm-redux/selectors/entities/integrations.ts b/app/mm-redux/selectors/entities/integrations.ts new file mode 100644 index 000000000..b6f1beac7 --- /dev/null +++ b/app/mm-redux/selectors/entities/integrations.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as reselect from 'reselect'; + +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import * as types from '@mm-redux/types'; + +export function getIncomingHooks(state: types.store.GlobalState) { + return state.entities.integrations.incomingHooks; +} + +export function getOutgoingHooks(state: types.store.GlobalState) { + return state.entities.integrations.outgoingHooks; +} + +export function getCommands(state: types.store.GlobalState) { + return state.entities.integrations.commands; +} + +export function getOAuthApps(state: types.store.GlobalState) { + return state.entities.integrations.oauthApps; +} + +export function getSystemCommands(state: types.store.GlobalState) { + return state.entities.integrations.systemCommands; +} + +/** + * get outgoing hooks in current team + */ +export const getOutgoingHooksInCurrentTeam = reselect.createSelector( + getCurrentTeamId, + getOutgoingHooks, + (teamId, hooks) => { + return Object.values(hooks).filter((o) => o.team_id === teamId); + } +); + +export const getAllCommands = reselect.createSelector( + getCommands, + getSystemCommands, + (commands, systemCommands) => { + return { + ...commands, + ...systemCommands, + }; + } +); + +export const getAutocompleteCommandsList = reselect.createSelector( + getAllCommands, + getCurrentTeamId, + (commands, currentTeamId) => { + return Object.values(commands).filter((command) => { + return command && (!command.team_id || command.team_id === currentTeamId) && command.auto_complete; + }).sort((a, b) => a.display_name.localeCompare(b.display_name)); + } +); diff --git a/app/mm-redux/selectors/entities/jobs.ts b/app/mm-redux/selectors/entities/jobs.ts new file mode 100644 index 000000000..d567b289c --- /dev/null +++ b/app/mm-redux/selectors/entities/jobs.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {GlobalState} from '@mm-redux/types/store'; +import {JobType, Job, JobsByType} from '@mm-redux/types/jobs'; +import {IDMappedObjects} from '@mm-redux/types/utilities'; +export function getAllJobs(state: GlobalState): IDMappedObjects { + return state.entities.jobs.jobs; +} + +export function getJobsByType(state: GlobalState): JobsByType { + return state.entities.jobs.jobsByTypeList; +} + +export function makeGetJobsByType(type: JobType): (state: GlobalState) => Array { + return createSelector( + getJobsByType, + (jobsByType) => { + return jobsByType[type] || []; + } + ); +} diff --git a/app/mm-redux/selectors/entities/posts.test.js b/app/mm-redux/selectors/entities/posts.test.js new file mode 100644 index 000000000..e1b6ea44e --- /dev/null +++ b/app/mm-redux/selectors/entities/posts.test.js @@ -0,0 +1,2419 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {Posts, Preferences} from '../../constants'; + +import * as Selectors from '@mm-redux/selectors/entities/posts'; +import {makeGetProfilesForReactions} from '@mm-redux/selectors/entities/users'; + +import TestHelper from 'test/test_helper'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; + +describe('Selectors.Posts', () => { + const user1 = TestHelper.fakeUserWithId(); + user1.notify_props = {}; + const profiles = {}; + profiles[user1.id] = user1; + + const posts = { + a: {id: 'a', channel_id: '1', create_at: 1, highlight: false, user_id: user1.id}, + b: {id: 'b', channel_id: '1', create_at: 2, highlight: false, user_id: user1.id}, + c: {id: 'c', root_id: 'a', channel_id: '1', create_at: 3, highlight: false, user_id: 'b'}, + d: {id: 'd', root_id: 'b', channel_id: '1', create_at: 4, highlight: false, user_id: 'b'}, + e: {id: 'e', root_id: 'a', channel_id: '1', create_at: 5, highlight: false, user_id: 'b'}, + f: {id: 'f', channel_id: '2', create_at: 6, highlight: false, user_id: 'b'}, + }; + + const reaction1 = {user_id: user1.id, emoji_name: '+1'}; + const reactionA = {[reaction1.user_id + '-' + reaction1.emoji_name]: reaction1}; + const reactions = { + a: reactionA, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts, + postsInChannel: { + 1: [ + {order: ['e', 'd', 'c', 'b', 'a'], recent: true}, + ], + 2: [ + {order: ['f'], recent: true}, + ], + }, + postsInThread: { + a: ['c', 'e'], + b: ['d'], + }, + reactions, + }, + preferences: { + myPreferences: {}, + }, + }, + }); + + it('should return the most recent post for each channel', () => { + const lastPosts = Selectors.getLastPostPerChannel(testState); + + assert.deepEqual(lastPosts, {1: posts.e, 2: posts.f}); + }); + + it('should return single post with no children', () => { + const getPostsForThread = Selectors.makeGetPostsForThread(); + + assert.deepEqual(getPostsForThread(testState, {channelId: '2', rootId: 'f'}), [posts.f]); + }); + + it('should return post with children', () => { + const getPostsForThread = Selectors.makeGetPostsForThread(); + + assert.deepEqual(getPostsForThread(testState, {channelId: '1', rootId: 'a'}), [posts.e, posts.c, posts.a]); + }); + + it('should return memoized result for identical props', () => { + const getPostsForThread = Selectors.makeGetPostsForThread(); + + const props = {channelId: '1', rootId: 'a'}; + const result = getPostsForThread(testState, props); + + assert.equal(result, getPostsForThread(testState, props)); + }); + + it('should return memoized result for multiple selectors with different props', () => { + const getPostsForThread1 = Selectors.makeGetPostsForThread(); + const getPostsForThread2 = Selectors.makeGetPostsForThread(); + + const props1 = {channelId: '1', rootId: 'a'}; + const result1 = getPostsForThread1(testState, props1); + + const props2 = {channelId: '1', rootId: 'b'}; + const result2 = getPostsForThread2(testState, props2); + + assert.equal(result1, getPostsForThread1(testState, props1)); + assert.equal(result2, getPostsForThread2(testState, props2)); + }); + + it('should return reactions for post', () => { + const getReactionsForPost = Selectors.makeGetReactionsForPost(); + assert.deepEqual(getReactionsForPost(testState, posts.a.id), reactionA); + }); + + it('should return profiles for reactions', () => { + const getProfilesForReactions = makeGetProfilesForReactions(); + assert.deepEqual(getProfilesForReactions(testState, [reaction1]), [user1]); + }); + + it('get posts in channel', () => { + const post1 = { + ...posts.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post2 = { + ...posts.b, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: true, + replyCount: 1, + isCommentMention: false, + }; + + const post3 = { + ...posts.c, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: false, + commentedOnPost: posts.a, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post4 = { + ...posts.d, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: posts.b, + consecutivePostByUser: true, + replyCount: 1, + isCommentMention: false, + }; + + const post5 = { + ...posts.e, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: posts.a, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: false, + }; + + const getPostsInChannel = Selectors.makeGetPostsInChannel(); + assert.deepEqual(getPostsInChannel(testState, '1', 30), [post5, post4, post3, post2, post1]); + }); + + it('get posts around post in channel', () => { + const post1 = { + ...posts.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post2 = { + ...posts.b, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: true, + replyCount: 1, + isCommentMention: false, + }; + + const post3 = { + ...posts.c, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: false, + commentedOnPost: posts.a, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + highlight: true, + }; + + const post4 = { + ...posts.d, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: posts.b, + consecutivePostByUser: true, + replyCount: 1, + isCommentMention: false, + }; + + const post5 = { + ...posts.e, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: posts.a, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: false, + }; + + const getPostsAroundPost = Selectors.makeGetPostsAroundPost(); + assert.deepEqual(getPostsAroundPost(testState, post3.id, '1'), [post5, post4, post3, post2, post1]); + }); + + it('get posts in channel with notify comments as any', () => { + const userAny = TestHelper.fakeUserWithId(); + userAny.notify_props = {comments: 'any'}; + const profilesAny = {}; + profilesAny[userAny.id] = userAny; + + const postsAny = { + a: {id: 'a', channel_id: '1', create_at: 1, highlight: false, user_id: userAny.id}, + b: {id: 'b', channel_id: '1', create_at: 2, highlight: false, user_id: 'b'}, + c: {id: 'c', root_id: 'a', channel_id: '1', create_at: 3, highlight: false, user_id: 'b'}, + d: {id: 'd', root_id: 'b', channel_id: '1', create_at: 4, highlight: false, user_id: userAny.id}, + e: {id: 'e', root_id: 'a', channel_id: '1', create_at: 5, highlight: false, user_id: 'b'}, + f: {id: 'f', root_id: 'b', channel_id: '1', create_at: 6, highlight: false, user_id: 'b'}, + g: {id: 'g', channel_id: '2', create_at: 7, highlight: false, user_id: 'b'}, + }; + + const testStateAny = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userAny.id, + profiles: profilesAny, + }, + posts: { + posts: postsAny, + postsInChannel: { + 1: [ + {order: ['f', 'e', 'd', 'c', 'b', 'a'], recent: true}, + ], + 2: [ + {order: ['g'], recent: true}, + ], + }, + postsInThread: { + a: ['c', 'e'], + b: ['d', 'f'], + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }); + + const post1 = { + ...postsAny.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post2 = { + ...postsAny.b, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: true, + }; + + const post3 = { + ...postsAny.c, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: false, + commentedOnPost: postsAny.a, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: true, + }; + + const post4 = { + ...postsAny.d, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsAny.b, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post5 = { + ...postsAny.e, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsAny.a, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: true, + }; + + const post6 = { + ...postsAny.f, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsAny.b, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: true, + }; + + const getPostsInChannel = Selectors.makeGetPostsInChannel(); + assert.deepEqual(getPostsInChannel(testStateAny, '1'), [post6, post5, post4, post3, post2, post1]); + }); + + it('get posts in channel with notify comments as root', () => { + const userRoot = TestHelper.fakeUserWithId(); + userRoot.notify_props = {comments: 'root'}; + const profilesRoot = {}; + profilesRoot[userRoot.id] = userRoot; + + const postsRoot = { + a: {id: 'a', channel_id: '1', create_at: 1, highlight: false, user_id: userRoot.id}, + b: {id: 'b', channel_id: '1', create_at: 2, highlight: false, user_id: 'b'}, + c: {id: 'c', root_id: 'a', channel_id: '1', create_at: 3, highlight: false, user_id: 'b'}, + d: {id: 'd', root_id: 'b', channel_id: '1', create_at: 4, highlight: false, user_id: userRoot.id}, + e: {id: 'e', root_id: 'a', channel_id: '1', create_at: 5, highlight: false, user_id: 'b'}, + f: {id: 'f', root_id: 'b', channel_id: '1', create_at: 6, highlight: false, user_id: 'b'}, + g: {id: 'g', channel_id: '2', create_at: 7, highlight: false, user_id: 'b'}, + }; + + const testStateRoot = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userRoot.id, + profiles: profilesRoot, + }, + posts: { + posts: postsRoot, + postsInChannel: { + 1: [ + {order: ['f', 'e', 'd', 'c', 'b', 'a'], recent: true}, + ], + 2: [ + {order: ['g'], recent: true}, + ], + }, + postsInThread: { + a: ['c', 'e'], + b: ['d', 'f'], + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }); + + const post1 = { + ...postsRoot.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post2 = { + ...postsRoot.b, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post3 = { + ...postsRoot.c, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: false, + commentedOnPost: postsRoot.a, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: true, + }; + + const post4 = { + ...postsRoot.d, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsRoot.b, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post5 = { + ...postsRoot.e, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsRoot.a, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: true, + }; + + const post6 = { + ...postsRoot.f, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsRoot.b, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: false, + }; + + const getPostsInChannel = Selectors.makeGetPostsInChannel(); + assert.deepEqual(getPostsInChannel(testStateRoot, '1'), [post6, post5, post4, post3, post2, post1]); + }); + + it('get posts in channel with notify comments as never', () => { + const userNever = TestHelper.fakeUserWithId(); + userNever.notify_props = {comments: 'never'}; + const profilesNever = {}; + profilesNever[userNever.id] = userNever; + + const postsNever = { + a: {id: 'a', channel_id: '1', create_at: 1, highlight: false, user_id: userNever.id}, + b: {id: 'b', channel_id: '1', create_at: 2, highlight: false, user_id: 'b'}, + c: {id: 'c', root_id: 'a', channel_id: '1', create_at: 3, highlight: false, user_id: 'b'}, + d: {id: 'd', root_id: 'b', channel_id: '1', create_at: 4, highlight: false, user_id: userNever.id}, + e: {id: 'e', root_id: 'a', channel_id: '1', create_at: 5, highlight: false, user_id: 'b'}, + f: {id: 'f', root_id: 'b', channel_id: '1', create_at: 6, highlight: false, user_id: 'b'}, + g: {id: 'g', channel_id: '2', create_at: 7, highlight: false, user_id: 'b'}, + }; + + const testStateNever = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userNever.id, + profiles: profilesNever, + }, + posts: { + posts: postsNever, + postsInChannel: { + 1: [ + {order: ['f', 'e', 'd', 'c', 'b', 'a'], recent: true}, + ], + 2: [ + {order: ['g'], recent: true}, + ], + }, + postsInThread: { + a: ['c', 'e'], + b: ['d', 'f'], + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }); + + const post1 = { + ...postsNever.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post2 = { + ...postsNever.b, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post3 = { + ...postsNever.c, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: false, + commentedOnPost: postsNever.a, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: false, + }; + + const post4 = { + ...postsNever.d, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsNever.b, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post5 = { + ...postsNever.e, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsNever.a, + consecutivePostByUser: false, + replyCount: 2, + isCommentMention: false, + }; + + const post6 = { + ...postsNever.f, + isFirstReply: true, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: postsNever.b, + consecutivePostByUser: true, + replyCount: 2, + isCommentMention: false, + }; + + const getPostsInChannel = Selectors.makeGetPostsInChannel(); + assert.deepEqual(getPostsInChannel(testStateNever, '1'), [post6, post5, post4, post3, post2, post1]); + }); + + it('gets posts around post in channel not adding ephemeral post to replyCount', () => { + const userAny = TestHelper.fakeUserWithId(); + userAny.notify_props = {comments: 'any'}; + const profilesAny = {}; + profilesAny[userAny.id] = userAny; + + const postsAny = { + a: {id: 'a', channel_id: '1', create_at: 1, highlight: false, user_id: userAny.id}, + b: {id: 'b', root_id: 'a', channel_id: '1', create_at: 2, highlight: false, user_id: 'b'}, + c: {id: 'c', root_id: 'a', channel_id: '1', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL}, + d: {id: 'd', channel_id: '2', create_at: 4, highlight: false, user_id: 'b'}, + }; + + const testStateAny = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userAny.id, + profiles: profilesAny, + }, + posts: { + posts: postsAny, + postsInChannel: { + 1: [ + {order: ['c', 'b', 'a'], recent: true}, + ], + 2: [ + {order: ['d'], recent: true}, + ], + }, + postsInThread: { + a: ['b', 'c'], + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }); + + const post1 = { + ...postsAny.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 1, + isCommentMention: false, + highlight: true, + }; + + const post2 = { + ...postsAny.b, + isFirstReply: true, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 1, + isCommentMention: true, + }; + + const post3 = { + ...postsAny.c, + isFirstReply: false, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 1, + isCommentMention: true, + }; + + const getPostsAroundPost = Selectors.makeGetPostsAroundPost(); + assert.deepEqual(getPostsAroundPost(testStateAny, post1.id, '1'), [post3, post2, post1]); + }); + + it('gets posts in channel not adding ephemeral post to replyCount', () => { + const userAny = TestHelper.fakeUserWithId(); + userAny.notify_props = {comments: 'any'}; + const profilesAny = {}; + profilesAny[userAny.id] = userAny; + + const postsAny = { + a: {id: 'a', channel_id: '1', create_at: 1, highlight: false, user_id: userAny.id}, + b: {id: 'b', root_id: 'a', channel_id: '1', create_at: 2, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL}, + c: {id: 'c', root_id: 'a', channel_id: '1', create_at: 3, highlight: false, user_id: 'b', state: Posts.POST_DELETED}, + d: {id: 'd', channel_id: '2', create_at: 4, highlight: false, user_id: 'b'}, + }; + + const testStateAny = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: userAny.id, + profiles: profilesAny, + }, + posts: { + posts: postsAny, + postsInChannel: { + 1: [ + {order: ['c', 'b', 'a'], recent: true}, + ], + 2: [ + {order: ['d'], recent: true}, + ], + }, + postsInThread: { + a: ['b', 'c'], + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }); + + const post1 = { + ...postsAny.a, + isFirstReply: false, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 0, + isCommentMention: false, + }; + + const post2 = { + ...postsAny.b, + isFirstReply: true, + isLastReply: false, + previousPostIsComment: false, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 0, + isCommentMention: true, + }; + + const post3 = { + ...postsAny.c, + isFirstReply: false, + isLastReply: true, + previousPostIsComment: true, + commentedOnPost: undefined, + consecutivePostByUser: false, + replyCount: 0, + isCommentMention: true, + }; + + const getPostsInChannel = Selectors.makeGetPostsInChannel(); + assert.deepEqual(getPostsInChannel(testStateAny, '1'), [post3, post2, post1]); + }); + + it('get current history item', () => { + const testState1 = deepFreezeAndThrowOnMutation({ + entities: { + posts: { + messagesHistory: { + messages: ['test1', 'test2', 'test3'], + index: { + post: 1, + comment: 2, + }, + }, + }, + }, + }); + + const testState2 = deepFreezeAndThrowOnMutation({ + entities: { + posts: { + messagesHistory: { + messages: ['test1', 'test2', 'test3'], + index: { + post: 0, + comment: 0, + }, + }, + }, + }, + }); + + const testState3 = deepFreezeAndThrowOnMutation({ + entities: { + posts: { + messagesHistory: { + messages: [], + index: { + post: -1, + comment: -1, + }, + }, + }, + }, + }); + + const getHistoryMessagePost = Selectors.makeGetMessageInHistoryItem(Posts.MESSAGE_TYPES.POST); + const getHistoryMessageComment = Selectors.makeGetMessageInHistoryItem(Posts.MESSAGE_TYPES.COMMENT); + assert.equal(getHistoryMessagePost(testState1), 'test2'); + assert.equal(getHistoryMessageComment(testState1), 'test3'); + assert.equal(getHistoryMessagePost(testState2), 'test1'); + assert.equal(getHistoryMessageComment(testState2), 'test1'); + assert.equal(getHistoryMessagePost(testState3), ''); + assert.equal(getHistoryMessageComment(testState3), ''); + }); + + describe('getPostIdsForThread', () => { + it('single post', () => { + const getPostIdsForThread = Selectors.makeGetPostIdsForThread(); + + const state = { + entities: { + posts: { + posts: { + 1001: {id: '1001', create_at: 1001}, + 1002: {id: '1002', create_at: 1002, root_id: '1001'}, + 1003: {id: '1003', create_at: 1003}, + 1004: {id: '1004', create_at: 1004, root_id: '1001'}, + 1005: {id: '1005', create_at: 1005}, + }, + postsInThread: { + 1001: ['1002', '1004'], + }, + }, + }, + }; + const expected = ['1005']; + + assert.deepEqual(getPostIdsForThread(state, '1005'), expected); + }); + + it('thread', () => { + const getPostIdsForThread = Selectors.makeGetPostIdsForThread(); + + const state = { + entities: { + posts: { + posts: { + 1001: {id: '1001', create_at: 1001}, + 1002: {id: '1002', create_at: 1002, root_id: '1001'}, + 1003: {id: '1003', create_at: 1003}, + 1004: {id: '1004', create_at: 1004, root_id: '1001'}, + 1005: {id: '1005', create_at: 1005}, + }, + postsInThread: { + 1001: ['1002', '1004'], + }, + }, + }, + }; + const expected = ['1004', '1002', '1001']; + + assert.deepEqual(getPostIdsForThread(state, '1001'), expected); + }); + + it('memoization', () => { + const getPostIdsForThread = Selectors.makeGetPostIdsForThread(); + + let state = { + entities: { + posts: { + posts: { + 1001: {id: '1001', create_at: 1001}, + 1002: {id: '1002', create_at: 1002, root_id: '1001'}, + 1003: {id: '1003', create_at: 1003}, + 1004: {id: '1004', create_at: 1004, root_id: '1001'}, + 1005: {id: '1005', create_at: 1005}, + }, + postsInThread: { + 1001: ['1002', '1004'], + }, + }, + }, + }; + + // One post, no changes + let previous = getPostIdsForThread(state, '1005'); + let now = getPostIdsForThread(state, '1005'); + assert.deepEqual(now, ['1005']); + assert.equal(now, previous); + + // One post, unrelated changes + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + 1006: {id: '1006', create_at: 1006, root_id: '1003'}, + }, + postsInThread: { + ...state.entities.posts.postsInThread, + 1003: ['1006'], + }, + }, + }, + }; + + previous = now; + now = getPostIdsForThread(state, '1005'); + assert.deepEqual(now, ['1005']); + assert.equal(now, previous); + + // One post, changes to post + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + 1005: {id: '1005', create_at: 1005, update_at: 1006}, + }, + postsInThread: state.entities.posts.postsInThread, + }, + }, + }; + + previous = now; + now = getPostIdsForThread(state, '1005'); + assert.deepEqual(now, ['1005']); + assert.equal(now, previous); + + // Change of thread + previous = now; + now = getPostIdsForThread(state, '1001'); + assert.deepEqual(now, ['1004', '1002', '1001']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsForThread(state, '1001'); + assert.equal(now, previous); + + // New post in thread + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + 1007: {id: '1007', create_at: 1007, root_id: '1001'}, + }, + postsInThread: { + ...state.entities.posts.postsInThread, + 1001: [...state.entities.posts.postsInThread['1001'], '1007'], + }, + }, + }, + }; + + previous = now; + now = getPostIdsForThread(state, '1001'); + assert.deepEqual(now, ['1007', '1004', '1002', '1001']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsForThread(state, '1001'); + assert.deepEqual(now, ['1007', '1004', '1002', '1001']); + assert.equal(now, previous); + }); + + it('memoization with multiple selectors', () => { + const getPostIdsForThread1 = Selectors.makeGetPostIdsForThread(); + const getPostIdsForThread2 = Selectors.makeGetPostIdsForThread(); + + const state = { + entities: { + posts: { + posts: { + 1001: {id: '1001', create_at: 1001}, + 1002: {id: '1002', create_at: 1002, root_id: '1001'}, + 1003: {id: '1003', create_at: 1003}, + 1004: {id: '1004', create_at: 1004, root_id: '1001'}, + 1005: {id: '1005', create_at: 1005}, + }, + postsInThread: { + 1001: ['1002', '1004'], + }, + }, + }, + }; + + let now1 = getPostIdsForThread1(state, '1001'); + let now2 = getPostIdsForThread2(state, '1001'); + assert.notEqual(now1, now2); + assert.deepEqual(now1, now2); + + let previous1 = now1; + now1 = getPostIdsForThread1(state, '1001'); + assert.equal(now1, previous1); + + const previous2 = now2; + now2 = getPostIdsForThread2(state, '1003'); + assert.notEqual(now2, previous2); + assert.notDeepEqual(now1, now2); + + previous1 = now1; + now1 = getPostIdsForThread1(state, '1001'); + assert.equal(now1, previous1); + }); + }); + + describe('getPostIdsAroundPost', () => { + it('no posts around', () => { + const getPostIdsAroundPost = Selectors.makeGetPostIdsAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostIdsAroundPost(state, 'a', '1234'), ['a']); + }); + + it('posts around', () => { + const getPostIdsAroundPost = Selectors.makeGetPostIdsAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostIdsAroundPost(state, 'c', '1234'), ['a', 'b', 'c', 'd', 'e']); + }); + + it('posts before limit', () => { + const getPostIdsAroundPost = Selectors.makeGetPostIdsAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostIdsAroundPost(state, 'a', '1234', {postsBeforeCount: 2}), ['a', 'b', 'c']); + }); + + it('posts after limit', () => { + const getPostIdsAroundPost = Selectors.makeGetPostIdsAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostIdsAroundPost(state, 'e', '1234', {postsAfterCount: 3}), ['b', 'c', 'd', 'e']); + }); + + it('posts before/after limit', () => { + const getPostIdsAroundPost = Selectors.makeGetPostIdsAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostIdsAroundPost(state, 'c', '1234', {postsBeforeCount: 2, postsAfterCount: 1}), ['b', 'c', 'd', 'e']); + }); + + it('memoization', () => { + const getPostIdsAroundPost = Selectors.makeGetPostIdsAroundPost(); + + let state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + }, + }; + + // No limit, no changes + let previous = getPostIdsAroundPost(state, 'c', '1234'); + let now = getPostIdsAroundPost(state, 'c', '1234'); + assert.deepEqual(now, ['a', 'b', 'c', 'd', 'e']); + assert.equal(now, previous); + + // Changes to posts in another channel + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + abcd: [ + {order: ['g', 'h', 'i', 'j', 'k', 'l'], recent: true}, + ], + }, + }, + }, + }; + + previous = now; + now = getPostIdsAroundPost(state, 'c', '1234'); + assert.deepEqual(now, ['a', 'b', 'c', 'd', 'e']); + assert.equal(now, previous); + + // Changes to posts in this channel + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + }, + }, + }; + + previous = now; + now = getPostIdsAroundPost(state, 'c', '1234'); + assert.deepEqual(now, ['a', 'b', 'c', 'd', 'e', 'f']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsAroundPost(state, 'c', '1234'); + assert.deepEqual(now, ['a', 'b', 'c', 'd', 'e', 'f']); + assert.equal(now, previous); + + // Change of channel + previous = now; + now = getPostIdsAroundPost(state, 'i', 'abcd'); + assert.deepEqual(now, ['g', 'h', 'i', 'j', 'k', 'l']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsAroundPost(state, 'i', 'abcd'); + assert.deepEqual(now, ['g', 'h', 'i', 'j', 'k', 'l']); + assert.equal(now, previous); + + // With limits + previous = now; + now = getPostIdsAroundPost(state, 'i', 'abcd', {postsBeforeCount: 2, postsAfterCount: 1}); + assert.deepEqual(now, ['h', 'i', 'j', 'k']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsAroundPost(state, 'i', 'abcd', {postsBeforeCount: 2, postsAfterCount: 1}); // Note that the options object is a new object each time + assert.deepEqual(now, ['h', 'i', 'j', 'k']); + assert.equal(now, previous); + + // Change of limits + previous = now; + now = getPostIdsAroundPost(state, 'i', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['g', 'h', 'i', 'j']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsAroundPost(state, 'i', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['g', 'h', 'i', 'j']); + assert.equal(now, previous); + + // Change of post + previous = now; + now = getPostIdsAroundPost(state, 'j', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['h', 'i', 'j', 'k']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsAroundPost(state, 'j', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['h', 'i', 'j', 'k']); + assert.equal(now, previous); + + // Change of posts past limit + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + abcd: [ + {order: ['y', 'g', 'h', 'i', 'j', 'k', 'l', 'f', 'z'], recent: true}, + ], + }, + }, + }, + }; + previous = now; + now = getPostIdsAroundPost(state, 'j', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['h', 'i', 'j', 'k']); + assert.equal(now, previous); + + // Change of post order + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + abcd: [ + {order: ['y', 'g', 'i', 'h', 'j', 'l', 'k', 'z'], recent: true}, + ], + }, + }, + }, + }; + + previous = now; + now = getPostIdsAroundPost(state, 'j', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['i', 'h', 'j', 'l']); + assert.notEqual(now, previous); + + previous = now; + now = getPostIdsAroundPost(state, 'j', 'abcd', {postsBeforeCount: 1, postsAfterCount: 2}); + assert.deepEqual(now, ['i', 'h', 'j', 'l']); + assert.equal(now, previous); + }); + + it('memoization with multiple selectors', () => { + const getPostIdsAroundPost1 = Selectors.makeGetPostIdsAroundPost(); + const getPostIdsAroundPost2 = Selectors.makeGetPostIdsAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + ], + abcd: [ + {order: ['g', 'h', 'i'], recent: true}, + ], + }, + }, + }, + }; + + const previous1 = getPostIdsAroundPost1(state, 'c', '1234'); + const previous2 = getPostIdsAroundPost2(state, 'h', 'abcd', {postsBeforeCount: 1, postsAfterCount: 0}); + + assert.notEqual(previous1, previous2); + + const now1 = getPostIdsAroundPost1(state, 'c', '1234'); + const now2 = getPostIdsAroundPost2(state, 'i', 'abcd', {postsBeforeCount: 1, postsAfterCount: 0}); + + assert.equal(now1, previous1); + assert.notEqual(now2, previous2); + assert.notEqual(now1, now2); + }); + }); + + describe('makeGetPostsChunkAroundPost', () => { + it('no posts around', () => { + const getPostsChunkAroundPost = Selectors.makeGetPostsChunkAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostsChunkAroundPost(state, 'a', '1234'), {order: ['a'], recent: true}); + }); + + it('posts around', () => { + const getPostsChunkAroundPost = Selectors.makeGetPostsChunkAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostsChunkAroundPost(state, 'c', '1234'), {order: ['a', 'b', 'c', 'd', 'e'], recent: true}); + }); + + it('no matching posts', () => { + const getPostsChunkAroundPost = Selectors.makeGetPostsChunkAroundPost(); + + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + }, + }, + }; + + assert.deepEqual(getPostsChunkAroundPost(state, 'noChunk', '1234'), null); + }); + + it('memoization', () => { + const getPostsChunkAroundPost = Selectors.makeGetPostsChunkAroundPost(); + + let state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + }, + }; + + // No limit, no changes + let previous = getPostsChunkAroundPost(state, 'c', '1234'); + + // Changes to posts in another channel + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + abcd: [ + {order: ['g', 'h', 'i', 'j', 'k', 'l'], recent: true}, + ], + }, + }, + }, + }; + + let now = getPostsChunkAroundPost(state, 'c', '1234'); + assert.deepEqual(now, {order: ['a', 'b', 'c', 'd', 'e'], recent: true}); + assert.equal(now, previous); + + // Changes to posts in this channel + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + }, + }, + }; + + previous = now; + now = getPostsChunkAroundPost(state, 'c', '1234'); + assert.deepEqual(now, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + assert.notEqual(now, previous); + + previous = now; + now = getPostsChunkAroundPost(state, 'c', '1234'); + assert.deepEqual(now, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + assert.equal(now, previous); + + // Change of channel + previous = now; + now = getPostsChunkAroundPost(state, 'i', 'abcd'); + assert.deepEqual(now, {order: ['g', 'h', 'i', 'j', 'k', 'l'], recent: true}); + assert.notEqual(now, previous); + + previous = now; + now = getPostsChunkAroundPost(state, 'i', 'abcd'); + assert.deepEqual(now, {order: ['g', 'h', 'i', 'j', 'k', 'l'], recent: true}); + assert.equal(now, previous); + + // Change of post in the chunk + previous = now; + now = getPostsChunkAroundPost(state, 'j', 'abcd'); + assert.deepEqual(now, {order: ['g', 'h', 'i', 'j', 'k', 'l'], recent: true}); + assert.equal(now, previous); + + // Change of post order + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + postsInChannel: { + ...state.entities.posts.postsInChannel, + abcd: [ + {order: ['y', 'g', 'i', 'h', 'j', 'l', 'k', 'z'], recent: true}, + ], + }, + }, + }, + }; + + previous = now; + now = getPostsChunkAroundPost(state, 'j', 'abcd'); + assert.deepEqual(now, {order: ['y', 'g', 'i', 'h', 'j', 'l', 'k', 'z'], recent: true}); + assert.notEqual(now, previous); + + previous = now; + now = getPostsChunkAroundPost(state, 'j', 'abcd'); + assert.deepEqual(now, {order: ['y', 'g', 'i', 'h', 'j', 'l', 'k', 'z'], recent: true}); + assert.equal(now, previous); + }); + }); + describe('getRecentPostsChunkInChannel', () => { + it('Should return as recent chunk exists', () => { + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + }, + }, + }; + + const recentPostsChunkInChannel = Selectors.getRecentPostsChunkInChannel(state, 1234); + assert.deepEqual(recentPostsChunkInChannel, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + }); + + it('Should return null as recent chunk does not exists', () => { + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: false}, + ], + }, + }, + }, + }; + + const recentPostsChunkInChannel = Selectors.getRecentPostsChunkInChannel(state, 1234); + assert.deepEqual(recentPostsChunkInChannel, null); + }); + }); + + describe('getOldestPostsChunkInChannel', () => { + it('Should return as oldest chunk exists', () => { + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], oldest: true, recent: true}, + ], + }, + }, + }, + }; + + const oldestPostsChunkInChannel = Selectors.getOldestPostsChunkInChannel(state, 1234); + assert.deepEqual(oldestPostsChunkInChannel, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true, oldest: true}); + }); + + it('Should return null as recent chunk does not exists', () => { + const state = { + entities: { + posts: { + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: false, oldest: false}, + ], + }, + }, + }, + }; + + const oldestPostsChunkInChannel = Selectors.getOldestPostsChunkInChannel(state, 1234); + assert.deepEqual(oldestPostsChunkInChannel, null); + }); + }); + + describe('getPostsChunkInChannelAroundTime', () => { + it('getPostsChunkInChannelAroundTime', () => { + const state = { + entities: { + posts: { + posts: { + e: {id: 'e', create_at: 1010}, + j: {id: 'j', create_at: 1001}, + a: {id: 'a', create_at: 1020}, + f: {id: 'f', create_at: 1010}, + }, + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + {order: ['e', 'f', 'g', 'h', 'i', 'j'], recent: false}, + ], + }, + }, + }, + }; + + const postsChunkInChannelAroundTime1 = Selectors.getPostsChunkInChannelAroundTime(state, 1234, 1011); + assert.deepEqual(postsChunkInChannelAroundTime1, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + + const postsChunkInChannelAroundTime2 = Selectors.getPostsChunkInChannelAroundTime(state, 1234, 1002); + assert.deepEqual(postsChunkInChannelAroundTime2, {order: ['e', 'f', 'g', 'h', 'i', 'j'], recent: false}); + + const postsChunkInChannelAroundTime3 = Selectors.getPostsChunkInChannelAroundTime(state, 1234, 1010); + assert.deepEqual(postsChunkInChannelAroundTime3, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + + const postsChunkInChannelAroundTime4 = Selectors.getPostsChunkInChannelAroundTime(state, 1234, 1020); + assert.deepEqual(postsChunkInChannelAroundTime4, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + }); + }); + + describe('getUnreadPostsChunk', () => { + it('should return recent chunk even if the timstamp is greater than the last post', () => { + const state = { + entities: { + posts: { + posts: { + e: {id: 'e', create_at: 1010}, + j: {id: 'j', create_at: 1001}, + a: {id: 'a', create_at: 1020}, + f: {id: 'f', create_at: 1010}, + }, + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + {order: ['e', 'f', 'g', 'h', 'i', 'j'], recent: false}, + ], + }, + }, + }, + }; + + const unreadPostsChunk = Selectors.getUnreadPostsChunk(state, 1234, 1030); + assert.deepEqual(unreadPostsChunk, {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}); + }); + + it('should return a not recent chunk based on the timestamp', () => { + const state = { + entities: { + posts: { + posts: { + e: {id: 'e', create_at: 1010}, + j: {id: 'j', create_at: 1001}, + a: {id: 'a', create_at: 1020}, + f: {id: 'f', create_at: 1010}, + }, + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c', 'd', 'e', 'f'], recent: true}, + {order: ['e', 'f', 'g', 'h', 'i', 'j'], recent: false}, + ], + }, + }, + }, + }; + + const unreadPostsChunk = Selectors.getUnreadPostsChunk(state, 1234, 1002); + assert.deepEqual(unreadPostsChunk, {order: ['e', 'f', 'g', 'h', 'i', 'j'], recent: false}); + }); + + it('should return recent chunk if it is an empty array', () => { + const state = { + entities: { + posts: { + posts: {}, + postsInChannel: { + 1234: [ + {order: [], recent: true}, + ], + }, + }, + }, + }; + + const unreadPostsChunk = Selectors.getUnreadPostsChunk(state, 1234, 1002); + assert.deepEqual(unreadPostsChunk, {order: [], recent: true}); + }); + + it('should return oldest chunk if timstamp greater than the oldest post', () => { + const state = { + entities: { + posts: { + posts: { + a: {id: 'a', create_at: 1001}, + b: {id: 'b', create_at: 1002}, + c: {id: 'c', create_at: 1003}, + }, + postsInChannel: { + 1234: [ + {order: ['a', 'b', 'c'], recent: true, oldest: true}, + ], + }, + }, + }, + }; + + const unreadPostsChunk = Selectors.getUnreadPostsChunk(state, 1234, 1000); + assert.deepEqual(unreadPostsChunk, {order: ['a', 'b', 'c'], recent: true, oldest: true}); + }); + }); + + describe('getPostsForIds', () => { + it('selector', () => { + const getPostsForIds = Selectors.makeGetPostsForIds(); + + const testPosts = { + 1000: {id: '1000'}, + 1001: {id: '1001'}, + 1002: {id: '1002'}, + 1003: {id: '1003'}, + 1004: {id: '1004'}, + }; + const state = { + entities: { + posts: { + posts: testPosts, + }, + }, + }; + + const postIds = ['1000', '1002', '1003']; + + const actual = getPostsForIds(state, postIds); + assert.equal(actual.length, 3); + assert.equal(actual[0], testPosts[postIds[0]]); + assert.equal(actual[1], testPosts[postIds[1]]); + assert.equal(actual[2], testPosts[postIds[2]]); + }); + + it('memoization', () => { + const getPostsForIds = Selectors.makeGetPostsForIds(); + + const testPosts = { + 1000: {id: '1000'}, + 1001: {id: '1001'}, + 1002: {id: '1002'}, + 1003: {id: '1003'}, + 1004: {id: '1004'}, + }; + let state = { + entities: { + posts: { + posts: { + ...testPosts, + }, + }, + }, + }; + let postIds = ['1000', '1002', '1003']; + + let now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1000'], testPosts['1002'], testPosts['1003']]); + + // No changes + let previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1000'], testPosts['1002'], testPosts['1003']]); + assert.equal(now, previous); + + // New, identical ids + postIds = ['1000', '1002', '1003']; + + previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1000'], testPosts['1002'], testPosts['1003']]); + assert.equal(now, previous); + + // New posts, no changes to ones in ids + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + }, + }, + }, + }; + + previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1000'], testPosts['1002'], testPosts['1003']]); + assert.equal(now, previous); + + // New ids + postIds = ['1001', '1002', '1004']; + + previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1001'], testPosts['1002'], testPosts['1004']]); + assert.notEqual(now, previous); + + previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1001'], testPosts['1002'], testPosts['1004']]); + assert.equal(now, previous); + + // New posts, changes to ones in ids + const newPost = {id: '1002', message: 'abcd'}; + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + [newPost.id]: newPost, + }, + }, + }, + }; + + previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1001'], newPost, testPosts['1004']]); + assert.notEqual(now, previous); + + previous = now; + now = getPostsForIds(state, postIds); + assert.deepEqual(now, [testPosts['1001'], newPost, testPosts['1004']]); + assert.equal(now, previous); + }); + }); + + describe('getMostRecentPostIdInChannel', () => { + it('system messages visible', () => { + const testPosts = { + 1000: {id: '1000', type: 'system_join_channel'}, + 1001: {id: '1001', type: 'system_join_channel'}, + 1002: {id: '1002'}, + 1003: {id: '1003'}, + }; + const state = { + entities: { + posts: { + posts: testPosts, + postsInChannel: { + channelId: [ + {order: ['1000', '1001', '1002', '1003'], recent: true}, + ], + }, + }, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_ADVANCED_SETTINGS}--${Preferences.ADVANCED_FILTER_JOIN_LEAVE}`]: {value: 'true'}, + }, + }, + }, + }; + + const postId = Selectors.getMostRecentPostIdInChannel(state, 'channelId'); + assert.equal(postId, '1000'); + }); + + it('system messages hidden', () => { + const testPosts = { + 1000: {id: '1000', type: 'system_join_channel'}, + 1001: {id: '1001', type: 'system_join_channel'}, + 1002: {id: '1002'}, + 1003: {id: '1003'}, + }; + const state = { + entities: { + posts: { + posts: testPosts, + postsInChannel: { + channelId: [ + {order: ['1000', '1001', '1002', '1003'], recent: true}, + ], + }, + }, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_ADVANCED_SETTINGS}--${Preferences.ADVANCED_FILTER_JOIN_LEAVE}`]: {value: 'false'}, + }, + }, + }, + }; + + const postId = Selectors.getMostRecentPostIdInChannel(state, 'channelId'); + assert.equal(postId, '1002'); + }); + }); + + describe('getLatestReplyablePostId', () => { + it('no posts', () => { + const state = { + entities: { + channels: { + currentChannelId: 'abcd', + }, + posts: { + posts: {}, + postsInChannel: [], + }, + preferences: { + myPreferences: {}, + }, + users: { + profiles: {}, + }, + }, + }; + const actual = Selectors.getLatestReplyablePostId(state); + + expect(actual).toEqual(''); + }); + + it('return first post which dosent have POST_DELETED state', () => { + const postsAny = { + a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'}, + b: {id: 'b', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', state: Posts.POST_DELETED}, + c: {id: 'c', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: 'system_join_channel'}, + d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL}, + e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'b'}, + }; + const state = { + entities: { + channels: { + currentChannelId: 'abcd', + }, + posts: { + posts: postsAny, + postsInChannel: { + abcd: [ + {order: ['b', 'c', 'd', 'e'], recent: true}, + ], + }, + }, + preferences: { + myPreferences: {}, + }, + users: { + profiles: {}, + }, + }, + }; + const actual = Selectors.getLatestReplyablePostId(state); + + expect(actual).toEqual(postsAny.e.id); + }); + }); + + describe('makeIsPostCommentMention', () => { + const currentUser = { + ...testState.entities.users.profiles[user1.id], + notify_props: { + comments: 'any', + }, + }; + + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profiles: { + ...testState.entities.users.profiles, + [user1.id]: currentUser, + }, + }, + }, + }; + + const isPostCommentMention = Selectors.makeIsPostCommentMention(); + + it('Should return true as root post is by the current user', () => { + assert.equal(isPostCommentMention(modifiedState, 'e'), true); + }); + + it('Should return false as post is not from currentUser', () => { + assert.equal(isPostCommentMention(modifiedState, 'b'), false); + }); + + it('Should return true as post is from webhook but user created rootPost', () => { + const modifiedWbhookPostState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + posts: { + ...modifiedState.entities.posts, + posts: { + ...modifiedState.entities.posts.posts, + e: { + ...modifiedState.entities.posts.posts.e, + props: { + from_webhook: true, + }, + user_id: user1.id, + }, + }, + }, + }, + }; + + assert.equal(isPostCommentMention(modifiedWbhookPostState, 'e'), true); + }); + + it('Should return true as user commented in the thread', () => { + const modifiedThreadState = { + ...modifiedState, + entities: { + ...modifiedState.entities, + posts: { + ...modifiedState.entities.posts, + posts: { + ...modifiedState.entities.posts.posts, + a: { + ...modifiedState.entities.posts.posts.a, + user_id: 'b', + }, + c: { + ...modifiedState.entities.posts.posts.c, + user_id: user1.id, + }, + }, + }, + }, + }; + + assert.equal(isPostCommentMention(modifiedThreadState, 'e'), true); + }); + + it('Should return false as user commented in the thread but notify_props is for root only', () => { + const modifiedCurrentUserForNotifyProps = { + ...testState.entities.users.profiles[user1.id], + notify_props: { + comments: 'root', + }, + }; + + const modifiedStateForRoot = { + ...modifiedState, + entities: { + ...modifiedState.entities, + posts: { + ...modifiedState.entities.posts, + posts: { + ...modifiedState.entities.posts.posts, + a: { + ...modifiedState.entities.posts.posts.a, + user_id: 'not current', + }, + c: { + ...modifiedState.entities.posts.posts.c, + user_id: user1.id, + }, + }, + }, + users: { + ...modifiedState.entities.users, + profiles: { + ...modifiedState.entities.users.profiles, + [user1.id]: modifiedCurrentUserForNotifyProps, + }, + }, + }, + }; + + assert.equal(isPostCommentMention(modifiedStateForRoot, 'e'), false); + }); + + it('Should return false as user created root post', () => { + const modifiedCurrentUserForNotifyProps = { + ...testState.entities.users.profiles[user1.id], + notify_props: { + comments: 'root', + }, + }; + + const modifiedStateForRoot = { + ...modifiedState, + entities: { + ...modifiedState.entities, + users: { + ...modifiedState.entities.users, + profiles: { + ...modifiedState.entities.users.profiles, + [user1.id]: modifiedCurrentUserForNotifyProps, + }, + }, + }, + }; + + assert.equal(isPostCommentMention(modifiedStateForRoot, 'e'), true); + }); + }); +}); + +describe('getPostIdsInCurrentChannel', () => { + test('should return null when channel is not loaded', () => { + const state = { + entities: { + channels: { + currentChannelId: 'channel1', + }, + posts: { + postsInChannel: {}, + }, + }, + }; + + const postIds = Selectors.getPostIdsInCurrentChannel(state); + + expect(postIds).toBe(null); + }); + + test('should return null when recent posts are not loaded', () => { + const state = { + entities: { + channels: { + currentChannelId: 'channel1', + }, + posts: { + postsInChannel: { + channel1: [ + {order: ['post1', 'post2']}, + ], + }, + }, + }, + }; + + const postIds = Selectors.getPostIdsInCurrentChannel(state); + + expect(postIds).toBe(null); + }); + + test('should return post order from recent block', () => { + const state = { + entities: { + channels: { + currentChannelId: 'channel1', + }, + posts: { + postsInChannel: { + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }, + }, + }, + }; + + const postIds = Selectors.getPostIdsInCurrentChannel(state); + + expect(postIds).toBe(state.entities.posts.postsInChannel.channel1[0].order); + }); +}); + +describe('getPostsInCurrentChannel', () => { + test('should return null when channel is not loaded', () => { + const state = { + entities: { + channels: { + currentChannelId: 'channel1', + }, + posts: { + posts: {}, + postsInChannel: {}, + postsInThread: {}, + }, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_ADVANCED_SETTINGS}--${Preferences.ADVANCED_FILTER_JOIN_LEAVE}`]: {value: 'true'}, + }, + }, + users: { + profiles: {}, + }, + }, + }; + + const postIds = Selectors.getPostsInCurrentChannel(state); + + expect(postIds).toEqual(null); + }); + + test('should return null when recent posts are not loaded', () => { + const post1 = {id: 'post1'}; + const post2 = {id: 'post2'}; + + const state = { + entities: { + channels: { + currentChannelId: 'channel1', + }, + posts: { + posts: { + post1, + post2, + }, + postsInChannel: { + channel1: [ + {order: ['post1', 'post2']}, + ], + }, + postsInThread: {}, + }, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_ADVANCED_SETTINGS}--${Preferences.ADVANCED_FILTER_JOIN_LEAVE}`]: {value: 'true'}, + }, + }, + users: { + profiles: {}, + }, + }, + }; + + const postIds = Selectors.getPostsInCurrentChannel(state); + + expect(postIds).toEqual(null); + }); + + test('should return post order from recent block', () => { + const post1 = {id: 'post1'}; + const post2 = {id: 'post2'}; + + const state = { + entities: { + channels: { + currentChannelId: 'channel1', + }, + posts: { + posts: { + post1, + post2, + }, + postsInChannel: { + channel1: [ + {order: ['post1', 'post2'], recent: true}, + ], + }, + postsInThread: {}, + }, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_ADVANCED_SETTINGS}--${Preferences.ADVANCED_FILTER_JOIN_LEAVE}`]: {value: 'true'}, + }, + }, + users: { + profiles: {}, + }, + }, + }; + + const postIds = Selectors.getPostsInCurrentChannel(state); + + expect(postIds).toMatchObject([post1, post2]); + }); +}); + +describe('getCurrentUsersLatestPost', () => { + const user1 = TestHelper.fakeUserWithId(); + user1.notify_props = {}; + const profiles = {}; + profiles[user1.id] = user1; + it('no posts', () => { + const noPosts = {}; + const state = { + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: noPosts, + postsInChannel: [], + }, + preferences: { + myPreferences: {}, + }, + channels: { + currentChannelId: 'abcd', + }, + }, + }; + const actual = Selectors.getCurrentUsersLatestPost(state); + + expect(actual).toEqual(null); + }); + + it('return first post which user can edit', () => { + const postsAny = { + a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'}, + b: {id: 'b', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', state: Posts.POST_DELETED}, + c: {id: 'c', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: 'system_join_channel'}, + d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL}, + e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'c'}, + f: {id: 'f', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id}, + }; + const state = { + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: postsAny, + postsInChannel: { + abcd: [ + {order: ['b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + postsInThread: {}, + }, + preferences: { + myPreferences: {}, + }, + channels: { + currentChannelId: 'abcd', + }, + }, + }; + const actual = Selectors.getCurrentUsersLatestPost(state); + + expect(actual).toMatchObject(postsAny.f); + }); + + it('return first post which user can edit ignore pending and failed', () => { + const postsAny = { + a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'}, + b: {id: 'b', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id, pending_post_id: 'b'}, + c: {id: 'c', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id, failed: true}, + d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL}, + e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'c'}, + f: {id: 'f', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id}, + }; + const state = { + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: postsAny, + postsInChannel: { + abcd: [ + {order: ['b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + postsInThread: {}, + }, + preferences: { + myPreferences: {}, + }, + channels: { + currentChannelId: 'abcd', + }, + }, + }; + const actual = Selectors.getCurrentUsersLatestPost(state); + + expect(actual).toMatchObject(postsAny.f); + }); + + it('return first post which has rootId match', () => { + const postsAny = { + a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'}, + b: {id: 'b', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', state: Posts.POST_DELETED}, + c: {id: 'c', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: 'system_join_channel'}, + d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL}, + e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'c'}, + f: {id: 'f', root_id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id}, + }; + const state = { + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: postsAny, + postsInChannel: { + abcd: [ + {order: ['b', 'c', 'd', 'e', 'f'], recent: true}, + ], + }, + postsInThread: {}, + }, + preferences: { + myPreferences: {}, + }, + channels: { + currentChannelId: 'abcd', + }, + }, + }; + const actual = Selectors.getCurrentUsersLatestPost(state, 'e'); + + expect(actual).toMatchObject(postsAny.f); + }); + + it('should not return posts outside of the recent block', () => { + const postsAny = { + a: {id: 'a', channel_id: 'a', create_at: 1, user_id: 'a'}, + }; + const state = { + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: postsAny, + postsInChannel: { + abcd: [ + {order: ['a'], recent: false}, + ], + }, + }, + preferences: { + myPreferences: {}, + }, + channels: { + currentChannelId: 'abcd', + }, + }, + }; + const actual = Selectors.getCurrentUsersLatestPost(state, 'e'); + + assert.equal(actual, null); + }); + + it('determine the sending posts', () => { + const state = { + entities: { + users: { + currentUserId: user1.id, + profiles, + }, + posts: { + posts: {}, + postsInChannel: {}, + pendingPostIds: ['1', '2', '3'], + }, + preferences: { + myPreferences: {}, + }, + channels: { + currentChannelId: 'abcd', + }, + }, + }; + + assert.equal(Selectors.isPostIdSending(state, '1'), true); + assert.equal(Selectors.isPostIdSending(state, '2'), true); + assert.equal(Selectors.isPostIdSending(state, '3'), true); + assert.equal(Selectors.isPostIdSending(state, '4'), false); + assert.equal(Selectors.isPostIdSending(state), false); + }); +}); + +describe('getExpandedLink', () => { + it('should get the expanded link from the state', () => { + const state = { + entities: { + posts: { + expandedURLs: { + a: 'b', + c: 'd', + }, + }, + }, + }; + assert.equal(Selectors.getExpandedLink(state, 'a'), 'b'); + assert.equal(Selectors.getExpandedLink(state, 'c'), 'd'); + }); + + it('should return undefined if it is not saved', () => { + const state = { + entities: { + posts: { + expandedURLs: { + a: 'b', + c: 'd', + }, + }, + }, + }; + assert.equal(Selectors.getExpandedLink(state, 'b'), undefined); + assert.equal(Selectors.getExpandedLink(state, ''), undefined); + }); +}); diff --git a/app/mm-redux/selectors/entities/posts.ts b/app/mm-redux/selectors/entities/posts.ts new file mode 100644 index 000000000..4cc6e322c --- /dev/null +++ b/app/mm-redux/selectors/entities/posts.ts @@ -0,0 +1,647 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {getCurrentUser} from '@mm-redux/selectors/entities/common'; +import {getMyPreferences} from '@mm-redux/selectors/entities/preferences'; +import {createIdsSelector} from '@mm-redux/utils/helpers'; +import {Posts, Preferences} from '../../constants'; +import {isPostEphemeral, isSystemMessage, shouldFilterJoinLeavePost, comparePosts, isPostPendingOrFailed, isPostCommentMention} from '@mm-redux/utils/post_utils'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; +import {GlobalState} from '@mm-redux/types/store'; +import {Post, PostWithFormatData, MessageHistory, PostOrderBlock} from '@mm-redux/types/posts'; +import {Reaction} from '@mm-redux/types/reactions'; +import {UserProfile} from '@mm-redux/types/users'; +import {Channel} from '@mm-redux/types/channels'; +import {$ID, IDMappedObjects, RelationOneToOne, RelationOneToMany, Dictionary} from '@mm-redux/types/utilities'; +export function getAllPosts(state: GlobalState) { + return state.entities.posts.posts; +} + +export function getPost(state: GlobalState, postId: $ID): Post { + return getAllPosts(state)[postId]; +} + +export function getPostsInThread(state: GlobalState): RelationOneToMany { + return state.entities.posts.postsInThread; +} + +export function getReactionsForPosts(state: GlobalState): RelationOneToOne { + return state.entities.posts.reactions; +} + +export function makeGetReactionsForPost(): (b: GlobalState, a: $ID) => { + [x: string]: Reaction; +} | undefined | null { + return createSelector(getReactionsForPosts, (state: GlobalState, postId: string) => postId, (reactions, postId) => { + if (reactions[postId]) { + return reactions[postId]; + } + + return null; + }); +} + +export function getOpenGraphMetadata(state: GlobalState): RelationOneToOne { + return state.entities.posts.openGraph; +} + +export function getOpenGraphMetadataForUrl(state: GlobalState, postId: string, url: string): object { + const openGraphForPost = state.entities.posts.openGraph[postId]; + return openGraphForPost ? openGraphForPost[url] : undefined; +} + +// getPostIdsInCurrentChannel returns the IDs of posts loaded at the bottom of the channel. It does not include older +// posts such as those loaded by viewing a thread or a permalink. + +export function getPostIdsInCurrentChannel(state: GlobalState): Array<$ID> | undefined | null { + return getPostIdsInChannel(state, state.entities.channels.currentChannelId); +} + +// getPostsInCurrentChannel returns the posts loaded at the bottom of the channel. It does not include older posts +// such as those loaded by viewing a thread or a permalink. + +export const getPostsInCurrentChannel: (a: GlobalState) => Array | undefined | null = (() => { + const getPostsInChannel = makeGetPostsInChannel(); + return (state: GlobalState) => getPostsInChannel(state, state.entities.channels.currentChannelId, -1); +})(); +export function makeGetPostIdsForThread(): (b: GlobalState, a: $ID) => Array<$ID> { + return createIdsSelector( + getAllPosts, + (state: GlobalState, rootId: string) => state.entities.posts.postsInThread[rootId] || [], + (state: GlobalState, rootId) => state.entities.posts.posts[rootId], + (posts, postsForThread, rootPost) => { + const thread: Post[] = []; + + if (rootPost) { + thread.push(rootPost); + } + + postsForThread.forEach((id) => { + const post = posts[id]; + if (post) { + thread.push(post); + } + }); + + thread.sort(comparePosts); + + return thread.map((post) => post.id); + } + ); +} + +export function makeGetPostsChunkAroundPost(): (c: GlobalState, b: $ID, a: $ID) => PostOrderBlock| null | undefined { + return createIdsSelector( + (state: GlobalState, postId: string, channelId: string) => state.entities.posts.postsInChannel[channelId], + (state: GlobalState, postId) => postId, + (postsForChannel, postId) => { + if (!postsForChannel) { + return null; + } + + let postChunk; + + for (const block of postsForChannel) { + const index = block.order.indexOf(postId); + + if (index === -1) { + continue; + } + + postChunk = block; + } + + return postChunk; + } + ); +} + +export function makeGetPostIdsAroundPost(): (d: GlobalState, c: $ID, b: $ID, a: { + postsBeforeCount: number; + postsAfterCount: number; +}) => Array<$ID> | undefined | null { + const getPostsChunkAroundPost = makeGetPostsChunkAroundPost(); + return createIdsSelector( + (state: GlobalState, postId: string, channelId: string) => getPostsChunkAroundPost(state, postId, channelId), + (state: GlobalState, postId) => postId, + (state: GlobalState, postId, channelId, options) => options && options.postsBeforeCount, + (state: GlobalState, postId, channelId, options) => options && options.postsAfterCount, + (postsChunk, postId, postsBeforeCount = Posts.POST_CHUNK_SIZE / 2, postsAfterCount = Posts.POST_CHUNK_SIZE / 2) => { + if (!postsChunk || !postsChunk.order) { + return null; + } + + const postIds = postsChunk.order; + const index = postIds.indexOf(postId); + + // Remember that posts that come after the post have a smaller index + const minPostIndex = postsAfterCount === -1 ? 0 : Math.max(index - postsAfterCount, 0); + const maxPostIndex = postsBeforeCount === -1 ? postIds.length : Math.min(index + postsBeforeCount + 1, postIds.length); // Needs the extra 1 to include the focused post + + return postIds.slice(minPostIndex, maxPostIndex); + } + ); +} + +function formatPostInChannel(post: Post, previousPost: Post | undefined | null, index: number, allPosts: IDMappedObjects, postsInThread: RelationOneToMany, postIds: Array<$ID>, currentUser: UserProfile, focusedPostId: $ID): PostWithFormatData { + let isFirstReply = false; + let isLastReply = false; + let highlight = false; + let commentedOnPost: Post| undefined; + + if (post.id === focusedPostId) { + highlight = true; + } + + if (post.root_id) { + if (previousPost && previousPost.root_id !== post.root_id) { + // Post is the first reply in a list of consecutive replies + isFirstReply = true; + + if (previousPost && previousPost.id !== post.root_id) { + commentedOnPost = allPosts[post.root_id]; + } + } + + if (index - 1 < 0 || allPosts[postIds[index - 1]].root_id !== post.root_id) { + // Post is the last reply in a list of consecutive replies + isLastReply = true; + } + } + + let previousPostIsComment = false; + + if (previousPost && previousPost.root_id) { + previousPostIsComment = true; + } + + const postFromWebhook = Boolean(post.props && post.props.from_webhook); + const prevPostFromWebhook = Boolean(previousPost && previousPost.props && previousPost.props.from_webhook); + let consecutivePostByUser = false; + if (previousPost && + previousPost.user_id === post.user_id && + post.create_at - previousPost.create_at <= Posts.POST_COLLAPSE_TIMEOUT && + !postFromWebhook && !prevPostFromWebhook && + !isSystemMessage(post) && !isSystemMessage(previousPost)) { + // The last post and this post were made by the same user within some time + consecutivePostByUser = true; + } + + let threadRepliedToByCurrentUser = false; + let replyCount = 0; + let isCommentMention = false; + + if (currentUser) { + const rootId = post.root_id || post.id; + const threadIds = postsInThread[rootId] || []; + + for (const pid of threadIds) { + const p = allPosts[pid]; + if (!p) { + continue; + } + + if (p.user_id === currentUser.id) { + threadRepliedToByCurrentUser = true; + } + + if (!isPostEphemeral(p)) { + replyCount += 1; + } + } + + const rootPost = allPosts[rootId]; + + isCommentMention = isPostCommentMention({post, currentUser, threadRepliedToByCurrentUser, rootPost}); + } + + return { + ...post, + isFirstReply, + isLastReply, + previousPostIsComment, + commentedOnPost, + consecutivePostByUser, + replyCount, + isCommentMention, + highlight, + }; +} + +// makeGetPostsInChannel creates a selector that returns up to the given number of posts loaded at the bottom of the +// given channel. It does not include older posts such as those loaded by viewing a thread or a permalink. + +export function makeGetPostsInChannel(): (c: GlobalState, b: $ID, a: number) => Array | undefined | null { + return createSelector( + getAllPosts, + getPostsInThread, + (state: GlobalState, channelId: $ID) => getPostIdsInChannel(state, channelId), + getCurrentUser, + getMyPreferences, + (state: GlobalState, channelId: $ID, numPosts: number) => numPosts || Posts.POST_CHUNK_SIZE, + (allPosts, postsInThread, allPostIds, currentUser, myPreferences, numPosts) => { + if (!allPostIds) { + return null; + } + + const posts: PostWithFormatData[] = []; + + const joinLeavePref = myPreferences[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]; + const showJoinLeave = joinLeavePref ? joinLeavePref.value !== 'false' : true; + + const postIds = numPosts === -1 ? allPostIds : allPostIds.slice(0, numPosts); + + for (let i = 0; i < postIds.length; i++) { + const post = allPosts[postIds[i]]; + + if (shouldFilterJoinLeavePost(post, showJoinLeave, currentUser ? currentUser.username : '')) { + continue; + } + + const previousPost = allPosts[postIds[i + 1]] || null; + posts.push(formatPostInChannel(post, previousPost, i, allPosts, postsInThread, postIds, currentUser, '')); + } + + return posts; + } + ); +} + +export function makeGetPostsAroundPost(): (c: GlobalState, b: $ID, a: $ID) => Array | undefined | null { + const getPostIdsAroundPost = makeGetPostIdsAroundPost(); + const options = { + postsBeforeCount: -1, // Where this is used in the web app, view state is used to determine how far back to display + postsAfterCount: Posts.POST_CHUNK_SIZE / 2, + }; + + return createSelector( + (state: GlobalState, focusedPostId: string, channelId: string) => getPostIdsAroundPost(state, focusedPostId, channelId, options), + getAllPosts, + getPostsInThread, + (state: GlobalState, focusedPostId) => focusedPostId, + getCurrentUser, + getMyPreferences, + (postIds, allPosts, postsInThread, focusedPostId, currentUser, myPreferences) => { + if (!postIds || !currentUser) { + return null; + } + + const posts: PostWithFormatData[] = []; + const joinLeavePref = myPreferences[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]; + const showJoinLeave = joinLeavePref ? joinLeavePref.value !== 'false' : true; + + for (let i = 0; i < postIds.length; i++) { + const post = allPosts[postIds[i]]; + + if (shouldFilterJoinLeavePost(post, showJoinLeave, currentUser.username)) { + continue; + } + + const previousPost = allPosts[postIds[i + 1]] || null; + const formattedPost = formatPostInChannel(post, previousPost, i, allPosts, postsInThread, postIds, currentUser, focusedPostId); + + posts.push(formattedPost); + } + + return posts; + } + ); +} + +// Returns a function that creates a creates a selector that will get the posts for a given thread. +// That selector will take a props object (containing a rootId field) as its +// only argument and will be memoized based on that argument. + +export function makeGetPostsForThread(): (b: GlobalState, a: { + rootId: $ID; +}) => Array { + return createSelector(getAllPosts, (state: GlobalState, { + rootId, + }: {rootId: string}) => state.entities.posts.postsInThread[rootId] || [], (state: GlobalState, { + rootId, + }) => state.entities.posts.posts[rootId], (posts, postsForThread, rootPost) => { + const thread: Post[] = []; + + if (rootPost) { + thread.push(rootPost); + } + + postsForThread.forEach((id) => { + const post = posts[id]; + + if (post) { + thread.push(post); + } + }); + thread.sort(comparePosts); + return thread; + }); +} + +export function makeGetCommentCountForPost(): (b: GlobalState, a: { + post: Post; +}) => number { + return createSelector( + getAllPosts, + (state: GlobalState, {post}: {post: Post}) => state.entities.posts.postsInThread[post ? post.id : ''] || [], + (state, props) => props, + (posts, postsForThread, {post: currentPost}) => { + if (!currentPost) { + return 0; + } + + let count = 0; + postsForThread.forEach((id) => { + const post = posts[id]; + if (post && post.state !== Posts.POST_DELETED && !isPostEphemeral(post)) { + count += 1; + } + }); + return count; + } + ); +} + +export const getSearchResults: (a: GlobalState) => Array = createSelector( + getAllPosts, + (state: GlobalState) => state.entities.search.results, + (posts, postIds) => { + if (!postIds) { + return []; + } + + return postIds.map((id) => posts[id]); + } +); + +// Returns the matched text from the search results, if the server has provided them. +// These matches will only be present if the server is running Mattermost 5.1 or higher +// with Elasticsearch enabled to search posts. Otherwise, null will be returned. + +export function getSearchMatches(state: GlobalState): { + [x: string]: Array; +} { + return state.entities.search.matches; +} + +export function makeGetMessageInHistoryItem(type: 'post'|'comment'): (a: GlobalState) => string { + return createSelector( + (state: GlobalState) => state.entities.posts.messagesHistory, + (messagesHistory: MessageHistory) => { + const idx = messagesHistory.index[type]; + const messages = messagesHistory.messages; + if (idx >= 0 && messages && messages.length > idx) { + return messages[idx]; + } + return ''; + } + ); +} + +export function makeGetPostsForIds(): (b: GlobalState, a: Array<$ID>) => Array { + return createIdsSelector( + getAllPosts, + (state: GlobalState, postIds: Array<$ID>) => postIds, + (allPosts, postIds) => { + if (!postIds) { + return []; + } + + return postIds.map((id) => allPosts[id]); + } + ); +} + +export const getLastPostPerChannel: (a: GlobalState) => RelationOneToOne = createSelector( + getAllPosts, + (state: GlobalState) => state.entities.posts.postsInChannel, + (allPosts, postsInChannel) => { + const ret: Dictionary = {}; + + for (const [channelId, postsForChannel] of Object.entries(postsInChannel)) { + const recentBlock = (postsForChannel).find((block) => block.recent); + if (!recentBlock) { + continue; + } + + const postId = recentBlock.order[0]; + if (allPosts.hasOwnProperty(postId)) { + ret[channelId] = allPosts[postId]; + } + } + + return ret; + } +); +export const getMostRecentPostIdInChannel: (b: GlobalState, a: $ID) => $ID | undefined | null = createSelector(getAllPosts, (state: GlobalState, channelId: string) => getPostIdsInChannel(state, channelId), getMyPreferences, (posts, postIdsInChannel, preferences) => { + if (!postIdsInChannel) { + return ''; + } + const key = getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE); + const allowSystemMessages = preferences[key] ? preferences[key].value === 'true' : true; + + if (!allowSystemMessages) { + // return the most recent non-system message in the channel + let postId; + for (let i = 0; i < postIdsInChannel.length; i++) { + const p = posts[postIdsInChannel[i]]; + if (!p.type || !p.type.startsWith(Posts.SYSTEM_MESSAGE_PREFIX)) { + postId = p.id; + break; + } + } + return postId; + } + + // return the most recent message in the channel + return postIdsInChannel[0]; +} +); + +export const getLatestReplyablePostId: (a: GlobalState) => $ID = createSelector( + getPostsInCurrentChannel, + (posts) => { + if (!posts) { + return ''; + } + + const latestReplyablePost = posts.find((post) => post.state !== Posts.POST_DELETED && !isSystemMessage(post) && !isPostEphemeral(post)); + if (!latestReplyablePost) { + return ''; + } + + return latestReplyablePost.id; + } +); + +export const getCurrentUsersLatestPost: (b: GlobalState, a: $ID) => PostWithFormatData | undefined | null = createSelector(getPostsInCurrentChannel, getCurrentUser, (_: any, rootId: string) => rootId, (posts, currentUser, rootId) => { + if (!posts) { + return null; + } + + const lastPost = posts.find((post) => { + // don't edit webhook posts, deleted posts, or system messages + if (post.user_id !== currentUser.id || post.props && post.props.from_webhook || post.state === Posts.POST_DELETED || isSystemMessage(post) || isPostEphemeral(post) || isPostPendingOrFailed(post)) { + return false; + } + + if (rootId) { + return post.root_id === rootId || post.id === rootId; + } + + return true; + }); + return lastPost; +}); + +export function getRecentPostsChunkInChannel(state: GlobalState, channelId: $ID): PostOrderBlock|null|undefined { + const postsForChannel = state.entities.posts.postsInChannel[channelId]; + + if (!postsForChannel) { + return null; + } + + return postsForChannel.find((block) => block.recent); +} + +export function getOldestPostsChunkInChannel(state: GlobalState, channelId: $ID): PostOrderBlock|null|undefined { + const postsForChannel = state.entities.posts.postsInChannel[channelId]; + + if (!postsForChannel) { + return null; + } + + return postsForChannel.find((block) => block.oldest); +} + +// getPostIdsInChannel returns the IDs of posts loaded at the bottom of the given channel. It does not include older +// posts such as those loaded by viewing a thread or a permalink. + +export function getPostIdsInChannel(state: GlobalState, channelId: $ID): Array<$ID> | undefined | null { + const recentBlock = getRecentPostsChunkInChannel(state, channelId); + + if (!recentBlock) { + return null; + } + + return recentBlock.order; +} + +export function getPostsChunkInChannelAroundTime(state: GlobalState, channelId: $ID, timeStamp: number): object | undefined | null { + const postsEntity = state.entities.posts; + const postsForChannel = postsEntity.postsInChannel[channelId]; + const posts = postsEntity.posts; + if (!postsForChannel) { + return null; + } + + const blockAroundTimestamp = postsForChannel.find((block) => { + const {order} = block; + const recentPostInBlock = posts[order[0]]; + const oldestPostInBlock = posts[order[order.length - 1]]; + if (recentPostInBlock && oldestPostInBlock) { + return (recentPostInBlock.create_at >= timeStamp && oldestPostInBlock.create_at <= timeStamp); + } + return false; + }); + + return blockAroundTimestamp; +} + +export function getUnreadPostsChunk(state: GlobalState, channelId: $ID, timeStamp: number): object | undefined | null { + const postsEntity = state.entities.posts; + const posts = postsEntity.posts; + const recentChunk = getRecentPostsChunkInChannel(state, channelId); + + /* 1. lastViewedAt can be greater than the most recent chunk in case of edited posts etc. + * return if recent block exists and oldest post is created after the last lastViewedAt timestamp + i.e all posts are read and the lastViewedAt is greater than the last post + + 2. lastViewedAt can be less than the first post in a channel if all the last viewed posts are deleted + * return if oldest block exist and oldest post created_at is greater than the last viewed post + i.e all posts are unread so the lastViewedAt is lessthan the first post + + The above two exceptions are added because we cannot select the chunk based on timestamp alone as these cases are out of bounds + + 3. Normal cases where there are few unreads and few reads in a chunk as that is how unread API returns data + * return getPostsChunkInChannelAroundTime + */ + + if (recentChunk) { + // This would happen if there are no posts in channel. + // If the system messages are deleted by sys admin. + // Experimental changes like hiding Join/Leave still will have recent chunk so it follows the default path based on timestamp + + if (!recentChunk.order.length) { + return recentChunk; + } + + const {order} = recentChunk; + const oldestPostInBlock = posts[order[order.length - 1]]; + + // check for only oldest posts because this can be higher than the latest post if the last post is edited + if (oldestPostInBlock.create_at <= timeStamp) { + return recentChunk; + } + } + + const oldestPostsChunk = getOldestPostsChunkInChannel(state, channelId); + + if (oldestPostsChunk && oldestPostsChunk.order.length) { + const {order} = oldestPostsChunk; + const oldestPostInBlock = posts[order[order.length - 1]]; + + if (oldestPostInBlock.create_at >= timeStamp) { + return oldestPostsChunk; + } + } + + return getPostsChunkInChannelAroundTime(state, channelId, timeStamp); +} + +export const isPostIdSending = (state: GlobalState, postId: $ID): boolean => + state.entities.posts.pendingPostIds.some((sendingPostId) => sendingPostId === postId); + +export const makeIsPostCommentMention = (): ((b: GlobalState, a: $ID) => boolean) => { + return createSelector( + getAllPosts, + getPostsInThread, + getCurrentUser, + getPost, + (allPosts, postsInThread, currentUser, post) => { + if (!post) { + return false; + } + + let threadRepliedToByCurrentUser = false; + let isCommentMention = false; + if (currentUser) { + const rootId = post.root_id || post.id; + const threadIds = postsInThread[rootId] || []; + + for (const pid of threadIds) { + const p = allPosts[pid]; + if (!p) { + continue; + } + + if (p.user_id === currentUser.id) { + threadRepliedToByCurrentUser = true; + } + } + + const rootPost = allPosts[rootId]; + + isCommentMention = isPostCommentMention({post, currentUser, threadRepliedToByCurrentUser, rootPost}); + } + + return isCommentMention; + } + ); +}; + +export function getExpandedLink(state: GlobalState, link: string): string { + return state.entities.posts.expandedURLs[link]; +} diff --git a/app/mm-redux/selectors/entities/preferences.test.js b/app/mm-redux/selectors/entities/preferences.test.js new file mode 100644 index 000000000..246958a37 --- /dev/null +++ b/app/mm-redux/selectors/entities/preferences.test.js @@ -0,0 +1,852 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {General, Preferences} from '../../constants'; + +import * as Selectors from '@mm-redux/selectors/entities/preferences'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; + +describe('Selectors.Preferences', () => { + const category1 = 'testcategory1'; + const category2 = 'testcategory2'; + const directCategory = Preferences.CATEGORY_DIRECT_CHANNEL_SHOW; + const groupCategory = Preferences.CATEGORY_GROUP_CHANNEL_SHOW; + const favCategory = Preferences.CATEGORY_FAVORITE_CHANNEL; + + const name1 = 'testname1'; + const value1 = 'true'; + const pref1 = {category: category1, name: name1, value: value1}; + + const name2 = 'testname2'; + const value2 = '42'; + const pref2 = {category: category2, name: name2, value: value2}; + + const dm1 = 'teammate1'; + const dmPref1 = {category: directCategory, name: dm1, value: 'true'}; + const dm2 = 'teammate2'; + const dmPref2 = {category: directCategory, name: dm2, value: 'false'}; + + const gp1 = 'group1'; + const prefGp1 = {category: groupCategory, name: gp1, value: 'true'}; + const gp2 = 'group2'; + const prefGp2 = {category: groupCategory, name: gp2, value: 'false'}; + + const fav1 = 'favorite1'; + const favPref1 = {category1: favCategory, name: fav1, value: 'true'}; + const fav2 = 'favorite2'; + const favPref2 = {category1: favCategory, name: fav2, value: 'false'}; + + const currentUserId = 'currentuserid'; + + const myPreferences = {}; + myPreferences[`${category1}--${name1}`] = pref1; + myPreferences[`${category2}--${name2}`] = pref2; + myPreferences[`${directCategory}--${dm1}`] = dmPref1; + myPreferences[`${directCategory}--${dm2}`] = dmPref2; + myPreferences[`${groupCategory}--${gp1}`] = prefGp1; + myPreferences[`${groupCategory}--${gp2}`] = prefGp2; + myPreferences[`${favCategory}--${fav1}`] = favPref1; + myPreferences[`${favCategory}--${fav2}`] = favPref2; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId, + }, + preferences: { + myPreferences, + }, + }, + }); + + describe('get preference', () => { + it('should return the requested value', () => { + assert.deepEqual(Selectors.get(testState, category1, name1), 'true'); + }); + + describe('should fallback to the default', () => { + it('if name unknown', () => { + assert.deepEqual(Selectors.get(testState, category1, 'unknown name'), ''); + }); + + it('if category unknown', () => { + assert.deepEqual(Selectors.get(testState, 'unknown category', name1), ''); + }); + }); + + describe('should fallback to the overridden default', () => { + it('if name unknown', () => { + assert.deepEqual(Selectors.get(testState, category1, 'unknown name', 'fallback'), 'fallback'); + }); + + it('if category unknown', () => { + assert.deepEqual(Selectors.get(testState, 'unknown category', name1, 'fallback'), 'fallback'); + }); + }); + }); + + describe('get bool preference', () => { + it('should return the requested value', () => { + assert.deepEqual(Selectors.getBool(testState, category1, name1), value1 === 'true'); + }); + + describe('should fallback to the default', () => { + it('if name unknown', () => { + assert.deepEqual(Selectors.getBool(testState, category1, 'unknown name'), false); + }); + + it('if category unknown', () => { + assert.deepEqual(Selectors.getBool(testState, 'unknown category', name1), false); + }); + }); + + describe('should fallback to the overridden default', () => { + it('if name unknown', () => { + assert.deepEqual(Selectors.getBool(testState, category1, 'unknown name', true), true); + }); + + it('if category unknown', () => { + assert.deepEqual(Selectors.getBool(testState, 'unknown category', name1, true), true); + }); + }); + }); + + describe('get int preference', () => { + it('should return the requested value', () => { + assert.deepEqual(Selectors.getInt(testState, category2, name2), value2); + }); + + describe('should fallback to the default', () => { + it('if name unknown', () => { + assert.deepEqual(Selectors.getInt(testState, category2, 'unknown name'), 0); + }); + + it('if category unknown', () => { + assert.deepEqual(Selectors.getInt(testState, 'unknown category', name2), 0); + }); + }); + + describe('should fallback to the overridden default', () => { + it('if name unknown', () => { + assert.deepEqual(Selectors.getInt(testState, category2, 'unknown name', 100), 100); + }); + + it('if category unknown', () => { + assert.deepEqual(Selectors.getInt(testState, 'unknown category', name2, 100), 100); + }); + }); + }); + + it('get preferences by category', () => { + const getCategory = Selectors.makeGetCategory(); + assert.deepEqual(getCategory(testState, category1), [pref1]); + }); + + it('get direct channel show preferences', () => { + assert.deepEqual(Selectors.getDirectShowPreferences(testState), [dmPref1, dmPref2]); + }); + + it('get group channel show preferences', () => { + assert.deepEqual(Selectors.getGroupShowPreferences(testState), [prefGp1, prefGp2]); + }); + + it('get teammate name display setting', () => { + assert.equal( + Selectors.getTeammateNameDisplaySetting({ + entities: { + general: { + config: { + TeammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_NICKNAME_FULLNAME, + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }), + General.TEAMMATE_NAME_DISPLAY.SHOW_NICKNAME_FULLNAME, + ); + }); + + describe('get theme', () => { + it('default theme', () => { + const currentTeamId = '1234'; + + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + }, + }, + }, + }), Preferences.THEMES.default); + }); + + it('custom theme', () => { + const currentTeamId = '1234'; + const theme = {sidebarBg: '#ff0000'}; + + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).sidebarBg, theme.sidebarBg); + }); + + it('team-specific theme', () => { + const currentTeamId = '1234'; + const otherTeamId = 'abcd'; + const theme = {sidebarBg: '#ff0000'}; + + assert.deepEqual(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify({}), + }, + [getPreferenceKey(Preferences.CATEGORY_THEME, currentTeamId)]: { + category: Preferences.CATEGORY_THEME, name: currentTeamId, value: JSON.stringify(theme), + }, + [getPreferenceKey(Preferences.CATEGORY_THEME, otherTeamId)]: { + category: Preferences.CATEGORY_THEME, name: otherTeamId, value: JSON.stringify({}), + }, + }, + }, + }, + }).sidebarBg, theme.sidebarBg); + }); + + it('mentionBj backwards compatability theme', () => { + const currentTeamId = '1234'; + const theme = {mentionBj: '#ff0000'}; + + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).mentionBg, theme.mentionBj); + + theme.mentionBg = '#ff0001'; + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).mentionBg, theme.mentionBg); + }); + + it('memoization', () => { + const currentTeamId = '1234'; + const otherTeamId = 'abcd'; + + let state = { + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify({}), + }, + [getPreferenceKey(Preferences.CATEGORY_THEME, currentTeamId)]: { + category: Preferences.CATEGORY_THEME, name: currentTeamId, value: JSON.stringify({sidebarBg: '#ff0000'}), + }, + [getPreferenceKey(Preferences.CATEGORY_THEME, otherTeamId)]: { + category: Preferences.CATEGORY_THEME, name: otherTeamId, value: JSON.stringify({}), + }, + }, + }, + }, + }; + + const before = Selectors.getTheme(state); + + assert.equal(before, Selectors.getTheme(state)); + + state = { + ...state, + entities: { + ...state.entities, + preferences: { + ...state.entities.preferences, + myPreferences: { + ...state.entities.preferences.myPreferences, + somethingUnrelated: { + category: 'somethingUnrelated', name: '', value: JSON.stringify({}), + }, + }, + }, + }, + }; + + assert.equal(before, Selectors.getTheme(state)); + + state = { + ...state, + entities: { + ...state.entities, + preferences: { + ...state.entities.preferences, + myPreferences: { + ...state.entities.preferences.myPreferences, + [getPreferenceKey(Preferences.CATEGORY_THEME, currentTeamId)]: { + category: Preferences.CATEGORY_THEME, name: currentTeamId, value: JSON.stringify({sidebarBg: '#0000ff'}), + }, + }, + }, + }, + }; + + assert.notEqual(before, Selectors.getTheme(state)); + assert.notDeepEqual(before, Selectors.getTheme(state)); + }); + + it('custom theme with upper case colours', () => { + const currentTeamId = '1234'; + const theme = {sidebarBg: '#FF0000'}; + + assert.deepEqual(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).sidebarBg, theme.sidebarBg.toLowerCase()); + }); + + it('custom theme with missing colours', () => { + const currentTeamId = '1234'; + const theme = {sidebarBg: '#ff0000'}; + + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).mentionHighlightLink, Preferences.THEMES.default.mentionHighlightLink); + }); + + it('system theme with missing colours', () => { + const currentTeamId = '1234'; + const theme = { + type: Preferences.THEMES.mattermostDark.type, + sidebarBg: '#ff0000', + }; + + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).sidebarText, Preferences.THEMES.mattermostDark.sidebarText); + }); + + it('non-default system theme', () => { + const currentTeamId = '1234'; + const theme = { + type: Preferences.THEMES.windows10.type, + }; + + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }).codeTheme, Preferences.THEMES.windows10.codeTheme); + }); + + it('should return the server-configured theme by default', () => { + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'mattermostDark', + }, + }, + teams: { + currentTeamId: null, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: null, + }, + }, + }, + }).codeTheme, Preferences.THEMES.mattermostDark.codeTheme); + + // Opposite case + assert.notEqual(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'windows10', + }, + }, + teams: { + currentTeamId: null, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: null, + }, + }, + }, + }).codeTheme, Preferences.THEMES.mattermostDark.codeTheme); + }); + + it('returns the "default" theme if the server-configured value is not present', () => { + assert.equal(Selectors.getTheme({ + entities: { + general: { + config: { + DefaultTheme: 'fakedoesnotexist', + }, + }, + teams: { + currentTeamId: null, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: null, + }, + }, + }, + }).codeTheme, Preferences.THEMES.default.codeTheme); + }); + }); + + it('get theme from style', () => { + const theme = {themeColor: '#ffffff'}; + const currentTeamId = '1234'; + + const state = { + entities: { + general: { + config: { + DefaultTheme: 'default', + }, + }, + teams: { + currentTeamId, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_THEME, '')]: { + category: Preferences.CATEGORY_THEME, name: '', value: JSON.stringify(theme), + }, + }, + }, + }, + }; + + function testStyleFunction(myTheme) { + return { + container: { + backgroundColor: myTheme.themeColor, + height: 100, + }, + }; + } + + const expected = { + container: { + backgroundColor: theme.themeColor, + height: 100, + }, + }; + + const getStyleFromTheme = Selectors.makeGetStyleFromTheme(); + + assert.deepEqual(getStyleFromTheme(state, testStyleFunction), expected); + }); + + it('get favorites names', () => { + assert.deepEqual(Selectors.getFavoritesPreferences(testState), [fav1]); + }); + + it('get visible teammates', () => { + assert.deepEqual(Selectors.getVisibleTeammate(testState), [dm1]); + }); + + it('get visible groups', () => { + assert.deepEqual(Selectors.getVisibleGroupIds(testState), [gp1]); + }); + + describe('get new sidebar preference', () => { + it('setting disabled, no user setting', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DISABLED, + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }), + false, + ); + }); + + it('setting disabled, user set to on', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DISABLED, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_ORGANIZATION)]: { + category: Preferences.CATEGORY_SIDEBAR_SETTINGS, name: Preferences.CHANNEL_SIDEBAR_ORGANIZATION, value: 'true', + }, + }, + }, + }, + }), + false, + ); + }); + + it('setting default_off, no user setting', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DEFAULT_OFF, + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }), + false, + ); + }); + + it('setting default_off, user set to off', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DEFAULT_OFF, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_ORGANIZATION)]: { + category: Preferences.CATEGORY_SIDEBAR_SETTINGS, name: Preferences.CHANNEL_SIDEBAR_ORGANIZATION, value: 'false', + }, + }, + }, + }, + }), + false, + ); + }); + + it('setting default_off, user set to on', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DEFAULT_OFF, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_ORGANIZATION)]: { + category: Preferences.CATEGORY_SIDEBAR_SETTINGS, name: Preferences.CHANNEL_SIDEBAR_ORGANIZATION, value: 'true', + }, + }, + }, + }, + }), + true, + ); + }); + + it('setting default_on, no user setting', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DEFAULT_ON, + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }), + true, + ); + }); + + it('setting default_on, user set to off', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DEFAULT_ON, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_ORGANIZATION)]: { + category: Preferences.CATEGORY_SIDEBAR_SETTINGS, name: Preferences.CHANNEL_SIDEBAR_ORGANIZATION, value: 'false', + }, + }, + }, + }, + }), + false, + ); + }); + + it('setting default_on, user set to on', () => { + assert.equal( + Selectors.getNewSidebarPreference({ + entities: { + general: { + config: { + ExperimentalChannelSidebarOrganization: General.DEFAULT_ON, + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_ORGANIZATION)]: { + category: Preferences.CATEGORY_SIDEBAR_SETTINGS, name: Preferences.CHANNEL_SIDEBAR_ORGANIZATION, value: 'true', + }, + }, + }, + }, + }), + true, + ); + }); + }); +}); + +describe('shouldAutocloseDMs', () => { + test('should return false by default', () => { + const state = { + entities: { + general: { + config: { + CloseUnusedDirectMessages: 'false', + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }; + + expect(Selectors.shouldAutocloseDMs(state)).toBe(false); + }); + + test('should return true when enabled by server but not set by user', () => { + const state = { + entities: { + general: { + config: { + CloseUnusedDirectMessages: 'true', + }, + }, + preferences: { + myPreferences: {}, + }, + }, + }; + + expect(Selectors.shouldAutocloseDMs(state)).toBe(true); + }); + + test('should return true when enabled by both server and user', () => { + const state = { + entities: { + general: { + config: { + CloseUnusedDirectMessages: 'true', + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]: {value: Preferences.AUTOCLOSE_DMS_ENABLED}, + }, + }, + }, + }; + + expect(Selectors.shouldAutocloseDMs(state)).toBe(true); + }); + + test('should return false when enabled by server but disabled by user', () => { + const state = { + entities: { + general: { + config: { + CloseUnusedDirectMessages: 'true', + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]: {value: ''}, + }, + }, + }, + }; + + expect(Selectors.shouldAutocloseDMs(state)).toBe(false); + }); + + test('should return false when enabled by user but disabled by server', () => { + const state = { + entities: { + general: { + config: { + CloseUnusedDirectMessages: 'false', + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]: {value: Preferences.AUTOCLOSE_DMS_ENABLED}, + }, + }, + }, + }; + + expect(Selectors.shouldAutocloseDMs(state)).toBe(false); + }); +}); diff --git a/app/mm-redux/selectors/entities/preferences.ts b/app/mm-redux/selectors/entities/preferences.ts new file mode 100644 index 000000000..8b8d570c9 --- /dev/null +++ b/app/mm-redux/selectors/entities/preferences.ts @@ -0,0 +1,267 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as reselect from 'reselect'; +import {General, Preferences} from '../../constants'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {createShallowSelector} from '@mm-redux/utils/helpers'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; +import {GlobalState} from '@mm-redux/types/store'; +import {PreferencesType, PreferenceType} from '@mm-redux/types/preferences'; + +export function getMyPreferences(state: GlobalState) { + return state.entities.preferences.myPreferences; +} + +export function get(state: GlobalState, category: string, name: string, defaultValue: any = '') { + const key = getPreferenceKey(category, name); + const prefs = getMyPreferences(state); + + if (!(key in prefs)) { + return defaultValue; + } + + return prefs[key].value; +} + +export function getBool(state: GlobalState, category: string, name: string, defaultValue = false) { + const value = get(state, category, name, String(defaultValue)); + return value !== 'false'; +} + +export function getInt(state: GlobalState, category: string, name: string, defaultValue = 0) { + const value = get(state, category, name, defaultValue); + return parseInt(value, 10); +} + +export function makeGetCategory() { + return reselect.createSelector( + getMyPreferences, + (state: GlobalState, category: string) => category, + (preferences, category) => { + const prefix = category + '--'; + const prefsInCategory: PreferenceType[] = []; + + for (const key in preferences) { + if (key.startsWith(prefix)) { + prefsInCategory.push(preferences[key]); + } + } + + return prefsInCategory; + } + ); +} + +const getDirectShowCategory = makeGetCategory(); + +export function getDirectShowPreferences(state: GlobalState) { + return getDirectShowCategory(state, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); +} + +const getGroupShowCategory = makeGetCategory(); + +export function getGroupShowPreferences(state: GlobalState) { + return getGroupShowCategory(state, Preferences.CATEGORY_GROUP_CHANNEL_SHOW); +} + +const getFavoritesCategory = makeGetCategory(); + +export function getFavoritesPreferences(state: GlobalState) { + const favorites = getFavoritesCategory(state, Preferences.CATEGORY_FAVORITE_CHANNEL); + return favorites.filter((f) => f.value === 'true').map((f) => f.name); +} + +export const getVisibleTeammate = reselect.createSelector( + getDirectShowPreferences, + (direct) => { + return direct.filter((dm) => dm.value === 'true' && dm.name).map((dm) => dm.name); + } +); + +export const getVisibleGroupIds = reselect.createSelector( + getGroupShowPreferences, + (groups) => { + return groups.filter((dm) => dm.value === 'true' && dm.name).map((dm) => dm.name); + } +); + +export const getTeammateNameDisplaySetting = reselect.createSelector( + getConfig, + getMyPreferences, + getLicense, + (config, preferences, license) => { + const useAdminTeammateNameDisplaySetting = (license && license.LockTeammateNameDisplay === 'true') && config.LockTeammateNameDisplay === 'true'; + const key = getPreferenceKey(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT); + if (preferences[key] && !useAdminTeammateNameDisplaySetting) { + return preferences[key].value; + } else if (config.TeammateNameDisplay) { + return config.TeammateNameDisplay; + } + return General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME; + } +); + +const getThemePreference = reselect.createSelector( + getMyPreferences, + getCurrentTeamId, + (myPreferences, currentTeamId) => { + // Prefer the user's current team-specific theme over the user's current global theme + let themePreference; + + if (currentTeamId) { + themePreference = myPreferences[getPreferenceKey(Preferences.CATEGORY_THEME, currentTeamId)]; + } + + if (!themePreference) { + themePreference = myPreferences[getPreferenceKey(Preferences.CATEGORY_THEME, '')]; + } + + return themePreference; + } +); + +const getDefaultTheme = reselect.createSelector(getConfig, (config) => { + if (config.DefaultTheme) { + const theme = Preferences.THEMES[config.DefaultTheme]; + if (theme) { + return theme; + } + } + + // If no config.DefaultTheme or value doesn't refer to a valid theme name... + return Preferences.THEMES.default; +}); + +export const getTheme = createShallowSelector( + getThemePreference, + getDefaultTheme, + (themePreference, defaultTheme) => { + let theme: any; + if (themePreference) { + theme = themePreference.value; + } else { + theme = defaultTheme; + } + + if (typeof theme === 'string') { + // A custom theme will be a JSON-serialized object stored in a preference + theme = JSON.parse(theme); + } + + // At this point, the theme should be a plain object + + // If this is a system theme, find it in case the user's theme is missing any fields + if (theme.type && theme.type !== 'custom') { + const match = Object.values(Preferences.THEMES).find((v: any) => v.type === theme.type) as any; + if (match) { + if (!match.mentionBg) { + match.mentionBg = match.mentionBj; + } + + return match; + } + } + + for (const key of Object.keys(defaultTheme)) { + if (theme[key]) { + // Fix a case where upper case theme colours are rendered as black + theme[key] = theme[key].toLowerCase(); + } + } + + // Backwards compatability with old name + if (!theme.mentionBg) { + theme.mentionBg = theme.mentionBj; + } + + return Object.assign({}, defaultTheme, theme); + } +); + +export function makeGetStyleFromTheme() { + return reselect.createSelector( + getTheme, + (state: GlobalState, getStyleFromTheme: Function) => getStyleFromTheme, + (theme, getStyleFromTheme) => { + return getStyleFromTheme(theme); + } + ); +} + +const defaultSidebarPrefs = { + grouping: 'by_type', + unreads_at_top: 'true', + favorite_at_top: 'true', + sorting: 'alpha', +}; + +export const getSidebarPreferences = reselect.createSelector( + (state: GlobalState) => { + const config = getConfig(state); + return config.ExperimentalGroupUnreadChannels !== General.DISABLED && getBool( + state, + Preferences.CATEGORY_SIDEBAR_SETTINGS, + 'show_unread_section', + config.ExperimentalGroupUnreadChannels === General.DEFAULT_ON + ); + }, + (state) => { + return get( + state, + Preferences.CATEGORY_SIDEBAR_SETTINGS, + '', + null + ); + }, + (showUnreadSection, sidebarPreference) => { + let sidebarPrefs = JSON.parse(sidebarPreference); + if (sidebarPrefs === null) { + // Support unread settings for old implementation + sidebarPrefs = { + ...defaultSidebarPrefs, + unreads_at_top: showUnreadSection ? 'true' : 'false', + }; + } + + return sidebarPrefs; + } +); + +export const getNewSidebarPreference = reselect.createSelector( + (state: GlobalState) => { + const config = getConfig(state); + return config.ExperimentalChannelSidebarOrganization; + }, + (state) => { + return get( + state, + Preferences.CATEGORY_SIDEBAR_SETTINGS, + Preferences.CHANNEL_SIDEBAR_ORGANIZATION, + null + ); + }, + (globalSetting, userSetting) => { + switch (globalSetting) { + case General.DISABLED: + return false; + case General.DEFAULT_ON: + return userSetting ? (userSetting === 'true') : true; + case General.DEFAULT_OFF: + return userSetting ? (userSetting === 'true') : false; + default: + return false; + } + } +); + +export function shouldAutocloseDMs(state: GlobalState) { + const config = getConfig(state); + if (!config.CloseUnusedDirectMessages || config.CloseUnusedDirectMessages === 'false') { + return false; + } + + const preference = get(state, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS, Preferences.AUTOCLOSE_DMS_ENABLED); + return preference === Preferences.AUTOCLOSE_DMS_ENABLED; +} diff --git a/app/mm-redux/selectors/entities/roles.test.js b/app/mm-redux/selectors/entities/roles.test.js new file mode 100644 index 000000000..88d00c75c --- /dev/null +++ b/app/mm-redux/selectors/entities/roles.test.js @@ -0,0 +1,228 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import TestHelper from 'test/test_helper'; +import * as Selectors from '@mm-redux/selectors/entities/roles'; +import {General} from '../../constants'; +import {getMySystemPermissions, getMySystemRoles, getRoles} from '@mm-redux/selectors/entities/roles_helpers'; + +describe('Selectors.Roles', () => { + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + const team3 = TestHelper.fakeTeamWithId(); + const myTeamMembers = {}; + myTeamMembers[team1.id] = {roles: 'test_team1_role1 test_team1_role2'}; + myTeamMembers[team2.id] = {roles: 'test_team2_role1 test_team2_role2'}; + myTeamMembers[team3.id] = {}; + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + channel1.display_name = 'Channel Name'; + + const channel2 = TestHelper.fakeChannelWithId(team1.id); + channel2.total_msg_count = 2; + channel2.display_name = 'DEF'; + + const channel3 = TestHelper.fakeChannelWithId(team2.id); + channel3.total_msg_count = 2; + + const channel4 = TestHelper.fakeChannelWithId(''); + channel4.display_name = 'Channel 4'; + + const channel5 = TestHelper.fakeChannelWithId(team1.id); + channel5.type = General.PRIVATE_CHANNEL; + channel5.display_name = 'Channel 5'; + + const channel6 = TestHelper.fakeChannelWithId(team1.id); + const channel7 = TestHelper.fakeChannelWithId(''); + channel7.display_name = ''; + channel7.type = General.GM_CHANNEL; + channel7.total_msg_count = 1; + + const channel8 = TestHelper.fakeChannelWithId(team1.id); + channel8.display_name = 'ABC'; + channel8.total_msg_count = 1; + + const channel9 = TestHelper.fakeChannelWithId(team1.id); + const channel10 = TestHelper.fakeChannelWithId(team1.id); + const channel11 = TestHelper.fakeChannelWithId(team1.id); + channel11.type = General.PRIVATE_CHANNEL; + const channel12 = TestHelper.fakeChannelWithId(team1.id); + + const channels = {}; + channels[channel1.id] = channel1; + channels[channel2.id] = channel2; + channels[channel3.id] = channel3; + channels[channel4.id] = channel4; + channels[channel5.id] = channel5; + channels[channel6.id] = channel6; + channels[channel7.id] = channel7; + channels[channel8.id] = channel8; + channels[channel9.id] = channel9; + channels[channel10.id] = channel10; + channels[channel11.id] = channel11; + channels[channel12.id] = channel12; + + const channelsInTeam = {}; + channelsInTeam[team1.id] = [channel1.id, channel2.id, channel5.id, channel6.id, channel8.id, channel10.id, channel11.id]; + channelsInTeam[team2.id] = [channel3.id]; + channelsInTeam[''] = [channel4.id, channel7.id, channel9.id]; + + const user = TestHelper.fakeUserWithId(); + const profiles = {}; + profiles[user.id] = user; + profiles[user.id].roles = 'test_user_role test_user_role2'; + + const myChannelMembers = {}; + myChannelMembers[channel1.id] = {roles: 'test_channel_a_role1 test_channel_a_role2'}; + myChannelMembers[channel2.id] = {roles: 'test_channel_a_role1 test_channel_a_role2'}; + myChannelMembers[channel3.id] = {roles: 'test_channel_a_role1 test_channel_a_role2'}; + myChannelMembers[channel4.id] = {roles: 'test_channel_a_role1 test_channel_a_role2'}; + myChannelMembers[channel5.id] = {roles: 'test_channel_a_role1 test_channel_a_role2'}; + myChannelMembers[channel7.id] = {roles: 'test_channel_b_role1 test_channel_b_role2'}; + myChannelMembers[channel8.id] = {roles: 'test_channel_b_role1 test_channel_b_role2'}; + myChannelMembers[channel9.id] = {roles: 'test_channel_b_role1 test_channel_b_role2'}; + myChannelMembers[channel10.id] = {roles: 'test_channel_c_role1 test_channel_c_role2'}; + myChannelMembers[channel11.id] = {roles: 'test_channel_c_role1 test_channel_c_role2'}; + myChannelMembers[channel12.id] = {}; + const roles = { + test_team1_role1: {permissions: ['team1_role1']}, + test_team2_role1: {permissions: ['team2_role1']}, + test_team2_role2: {permissions: ['team2_role2']}, + test_channel_a_role1: {permissions: ['channel_a_role1']}, + test_channel_a_role2: {permissions: ['channel_a_role2']}, + test_channel_b_role2: {permissions: ['channel_b_role2']}, + test_channel_c_role1: {permissions: ['channel_c_role1']}, + test_channel_c_role2: {permissions: ['channel_c_role2']}, + test_user_role2: {permissions: ['user_role2']}, + }; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + }, + teams: { + currentTeamId: team1.id, + myMembers: myTeamMembers, + }, + channels: { + currentChannelId: channel1.id, + channels, + myMembers: myChannelMembers, + }, + roles: { + roles, + }, + }, + }); + + it('should return my roles by scope on getMyRoles/getMySystemRoles/getMyTeamRoles/getMyChannelRoles', () => { + const teamsRoles = {}; + teamsRoles[team1.id] = new Set(['test_team1_role1', 'test_team1_role2']); + teamsRoles[team2.id] = new Set(['test_team2_role1', 'test_team2_role2']); + const channelsRoles = {}; + channelsRoles[channel1.id] = new Set(['test_channel_a_role1', 'test_channel_a_role2']); + channelsRoles[channel2.id] = new Set(['test_channel_a_role1', 'test_channel_a_role2']); + channelsRoles[channel3.id] = new Set(['test_channel_a_role1', 'test_channel_a_role2']); + channelsRoles[channel4.id] = new Set(['test_channel_a_role1', 'test_channel_a_role2']); + channelsRoles[channel5.id] = new Set(['test_channel_a_role1', 'test_channel_a_role2']); + channelsRoles[channel7.id] = new Set(['test_channel_b_role1', 'test_channel_b_role2']); + channelsRoles[channel8.id] = new Set(['test_channel_b_role1', 'test_channel_b_role2']); + channelsRoles[channel9.id] = new Set(['test_channel_b_role1', 'test_channel_b_role2']); + channelsRoles[channel10.id] = new Set(['test_channel_c_role1', 'test_channel_c_role2']); + channelsRoles[channel11.id] = new Set(['test_channel_c_role1', 'test_channel_c_role2']); + const myRoles = { + system: new Set(['test_user_role', 'test_user_role2']), + team: teamsRoles, + channel: channelsRoles, + }; + assert.deepEqual(Selectors.getMyRoles(testState), myRoles); + assert.deepEqual(getMySystemRoles(testState), myRoles.system); + assert.deepEqual(Selectors.getMyTeamRoles(testState), myRoles.team); + assert.deepEqual(Selectors.getMyChannelRoles(testState), myRoles.channel); + }); + + it('should return current loaded roles on getRoles', () => { + const loadedRoles = { + test_team1_role1: {permissions: ['team1_role1']}, + test_team2_role1: {permissions: ['team2_role1']}, + test_team2_role2: {permissions: ['team2_role2']}, + test_channel_a_role1: {permissions: ['channel_a_role1']}, + test_channel_a_role2: {permissions: ['channel_a_role2']}, + test_channel_b_role2: {permissions: ['channel_b_role2']}, + test_channel_c_role1: {permissions: ['channel_c_role1']}, + test_channel_c_role2: {permissions: ['channel_c_role2']}, + test_user_role2: {permissions: ['user_role2']}, + }; + assert.deepEqual(getRoles(testState), loadedRoles); + }); + + it('should return my system permission on getMySystemPermissions', () => { + assert.deepEqual(getMySystemPermissions(testState), new Set([ + 'user_role2', + ])); + }); + + it('should return if i have a system permission on haveISystemPermission', () => { + assert.equal(Selectors.haveISystemPermission(testState, {permission: 'user_role2'}), true); + assert.equal(Selectors.haveISystemPermission(testState, {permission: 'invalid_permission'}), false); + }); + + it('should return my team permission on getMyTeamPermissions', () => { + assert.deepEqual(Selectors.getMyTeamPermissions(testState, {team: team1.id}), new Set([ + 'user_role2', 'team1_role1', + ])); + }); + + it('should return if i have a team permission on haveITeamPermission', () => { + assert.equal(Selectors.haveITeamPermission(testState, {team: team1.id, permission: 'user_role2'}), true); + assert.equal(Selectors.haveITeamPermission(testState, {team: team1.id, permission: 'team1_role1'}), true); + assert.equal(Selectors.haveITeamPermission(testState, {team: team1.id, permission: 'team2_role2'}), false); + assert.equal(Selectors.haveITeamPermission(testState, {team: team1.id, permission: 'invalid_permission'}), false); + }); + + it('should return my team permission on getMyCurrentTeamPermissions', () => { + assert.deepEqual(Selectors.getMyCurrentTeamPermissions(testState), new Set([ + 'user_role2', 'team1_role1', + ])); + }); + + it('should return if i have a team permission on haveICurrentTeamPermission', () => { + assert.equal(Selectors.haveICurrentTeamPermission(testState, {permission: 'user_role2'}), true); + assert.equal(Selectors.haveICurrentTeamPermission(testState, {permission: 'team1_role1'}), true); + assert.equal(Selectors.haveICurrentTeamPermission(testState, {permission: 'team2_role2'}), false); + assert.equal(Selectors.haveICurrentTeamPermission(testState, {permission: 'invalid_permission'}), false); + }); + + it('should return my channel permission on getMyChannelPermissions', () => { + assert.deepEqual(Selectors.getMyChannelPermissions(testState, {team: team1.id, channel: channel1.id}), new Set([ + 'user_role2', 'team1_role1', 'channel_a_role1', 'channel_a_role2', + ])); + }); + + it('should return if i have a channel permission on haveIChannelPermission', () => { + assert.equal(Selectors.haveIChannelPermission(testState, {team: team1.id, channel: channel1.id, permission: 'user_role2'}), true); + assert.equal(Selectors.haveIChannelPermission(testState, {team: team1.id, channel: channel1.id, permission: 'team1_role1'}), true); + assert.equal(Selectors.haveIChannelPermission(testState, {team: team1.id, channel: channel1.id, permission: 'team2_role2'}), false); + assert.equal(Selectors.haveIChannelPermission(testState, {team: team1.id, channel: channel1.id, permission: 'channel_a_role1'}), true); + assert.equal(Selectors.haveIChannelPermission(testState, {team: team1.id, channel: channel1.id, permission: 'channel_b_role1'}), false); + }); + + it('should return my current channel permission on getMyCurrentChannelPermissions', () => { + assert.deepEqual(Selectors.getMyCurrentChannelPermissions(testState), new Set([ + 'user_role2', 'team1_role1', 'channel_a_role1', 'channel_a_role2', + ])); + }); + + it('should return if i have a channel permission on haveICurrentChannelPermission', () => { + assert.equal(Selectors.haveICurrentChannelPermission(testState, {permission: 'user_role2'}), true); + assert.equal(Selectors.haveICurrentChannelPermission(testState, {permission: 'team1_role1'}), true); + assert.equal(Selectors.haveICurrentChannelPermission(testState, {permission: 'team2_role2'}), false); + assert.equal(Selectors.haveICurrentChannelPermission(testState, {permission: 'channel_a_role1'}), true); + assert.equal(Selectors.haveICurrentChannelPermission(testState, {permission: 'channel_b_role1'}), false); + }); +}); diff --git a/app/mm-redux/selectors/entities/roles.ts b/app/mm-redux/selectors/entities/roles.ts new file mode 100644 index 000000000..2fa1ea367 --- /dev/null +++ b/app/mm-redux/selectors/entities/roles.ts @@ -0,0 +1,200 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import * as reselect from 'reselect'; +import {getCurrentUser, getCurrentChannelId} from '@mm-redux/selectors/entities/common'; +import {getTeamMemberships, getCurrentTeamId} from './teams'; +import * as types from '@mm-redux/types'; +import {getMySystemPermissions, getMySystemRoles, getRoles, PermissionsOptions} from '@mm-redux/selectors/entities/roles_helpers'; +import {GlobalState} from '@mm-redux/types/store'; +import {Dictionary} from '@mm-redux/types/utilities'; +import {Role} from '@mm-redux/types/roles'; +import {Channel} from '@mm-redux/types/channels'; +import {Team} from '@mm-redux/types/teams'; + +export {getMySystemPermissions, getMySystemRoles, getRoles}; + +export const getMyTeamRoles = reselect.createSelector( + getTeamMemberships, + (teamsMemberships) => { + const roles: Dictionary> = {}; + if (teamsMemberships) { + for (const key in teamsMemberships) { + if (teamsMemberships.hasOwnProperty(key) && teamsMemberships[key].roles) { + roles[key] = new Set(teamsMemberships[key].roles.split(' ')); + } + } + } + return roles; + } +); + +export const getMyChannelRoles = reselect.createSelector( + (state: types.store.GlobalState) => state.entities.channels.myMembers, + (channelsMemberships) => { + const roles: Dictionary> = {}; + if (channelsMemberships) { + for (const key in channelsMemberships) { + if (channelsMemberships.hasOwnProperty(key) && channelsMemberships[key].roles) { + roles[key] = new Set(channelsMemberships[key].roles.split(' ')); + } + } + } + return roles; + } +); + +export const getMyRoles = reselect.createSelector( + getMySystemRoles, + getMyTeamRoles, + getMyChannelRoles, + (systemRoles, teamRoles, channelRoles) => { + return { + system: systemRoles, + team: teamRoles, + channel: channelRoles, + }; + } +); + +export const getRolesById = reselect.createSelector( + getRoles, + (rolesByName) => { + const rolesById: Dictionary = {}; + for (const role of Object.values(rolesByName)) { + rolesById[role.id] = role; + } + return rolesById; + } +); + +export const getMyCurrentTeamPermissions = reselect.createSelector( + getMyTeamRoles, + getRoles, + getMySystemPermissions, + getCurrentTeamId, + (myTeamRoles, roles, systemPermissions, teamId) => { + const permissions = new Set(); + if (myTeamRoles[teamId]) { + for (const roleName of myTeamRoles[teamId]) { + if (roles[roleName]) { + for (const permission of roles[roleName].permissions) { + permissions.add(permission); + } + } + } + } + for (const permission of systemPermissions) { + permissions.add(permission); + } + return permissions; + } +); + +export const getMyCurrentChannelPermissions = reselect.createSelector( + getMyChannelRoles, + getRoles, + getMyCurrentTeamPermissions, + getCurrentChannelId, + (myChannelRoles, roles, teamPermissions, channelId) => { + const permissions = new Set(); + if (myChannelRoles[channelId]) { + for (const roleName of myChannelRoles[channelId]) { + if (roles[roleName]) { + for (const permission of roles[roleName].permissions) { + permissions.add(permission); + } + } + } + } + for (const permission of teamPermissions) { + permissions.add(permission); + } + return permissions; + } +); + +export const getMyTeamPermissions = reselect.createSelector( + getMyTeamRoles, + getRoles, + getMySystemPermissions, + (state: GlobalState, options: PermissionsOptions) => options.team, + (myTeamRoles, roles, systemPermissions, teamId) => { + const permissions = new Set(); + if (myTeamRoles[teamId!]) { + for (const roleName of myTeamRoles[teamId!]) { + if (roles[roleName]) { + for (const permission of roles[roleName].permissions) { + permissions.add(permission); + } + } + } + } + for (const permission of systemPermissions) { + permissions.add(permission); + } + return permissions; + } +); + +export const getMyChannelPermissions = reselect.createSelector( + getMyChannelRoles, + getRoles, + getMyTeamPermissions, + (state, options: PermissionsOptions) => options.channel, + (myChannelRoles, roles, teamPermissions, channelId) => { + const permissions = new Set(); + if (myChannelRoles[channelId!]) { + for (const roleName of myChannelRoles[channelId!]) { + if (roles[roleName]) { + for (const permission of roles[roleName].permissions) { + permissions.add(permission); + } + } + } + } + for (const permission of teamPermissions) { + permissions.add(permission); + } + return permissions; + } +); + +export const haveISystemPermission = reselect.createSelector( + getMySystemPermissions, + (state: GlobalState, options: PermissionsOptions) => options.permission, + (permissions, permission) => { + return permissions.has(permission); + } +); + +export const haveITeamPermission = reselect.createSelector( + getMyTeamPermissions, + (state, options) => options.permission, + (permissions, permission) => { + return permissions.has(permission); + } +); + +export const haveIChannelPermission = reselect.createSelector( + getMyChannelPermissions, + (state, options) => options.permission, + (permissions, permission) => { + return permissions.has(permission); + } +); + +export const haveICurrentTeamPermission = reselect.createSelector( + getMyCurrentTeamPermissions, + (state: GlobalState, options: PermissionsOptions) => options.permission, + (permissions, permission) => { + return permissions.has(permission); + } +); + +export const haveICurrentChannelPermission = reselect.createSelector( + getMyCurrentChannelPermissions, + (state: GlobalState, options: PermissionsOptions) => options.permission, + (permissions, permission) => { + return permissions.has(permission); + } +); diff --git a/app/mm-redux/selectors/entities/roles_helpers.ts b/app/mm-redux/selectors/entities/roles_helpers.ts new file mode 100644 index 000000000..81a9b85fd --- /dev/null +++ b/app/mm-redux/selectors/entities/roles_helpers.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import * as reselect from 'reselect'; +import {GlobalState} from '@mm-redux/types/store'; +import {getCurrentUser} from '@mm-redux/selectors/entities/common'; +import * as types from '@mm-redux/types'; + +export type PermissionsOptions = { + channel?: string; + team?: string; + permission: string; +}; + +export function getRoles(state: GlobalState) { + return state.entities.roles.roles; +} + +export const getMySystemRoles = reselect.createSelector(getCurrentUser, (user: types.users.UserProfile) => { + if (user) { + return new Set(user.roles.split(' ')); + } + + return new Set(); +}); + +export const getMySystemPermissions = reselect.createSelector(getMySystemRoles, getRoles, (mySystemRoles: Set, roles) => { + const permissions = new Set(); + + for (const roleName of mySystemRoles) { + if (roles[roleName]) { + for (const permission of roles[roleName].permissions) { + permissions.add(permission); + } + } + } + + return permissions; +}); + +export const haveISystemPermission = reselect.createSelector(getMySystemPermissions, (state: GlobalState, options: PermissionsOptions) => options.permission, (permissions, permission) => { + return permissions.has(permission); +}); diff --git a/app/mm-redux/selectors/entities/schemes.test.js b/app/mm-redux/selectors/entities/schemes.test.js new file mode 100644 index 000000000..612cca0e9 --- /dev/null +++ b/app/mm-redux/selectors/entities/schemes.test.js @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import TestHelper from 'test/test_helper'; +import * as Selectors from '@mm-redux/selectors/entities/schemes'; +import {ScopeTypes} from '@mm-redux/constants/schemes'; + +describe('Selectors.Schemes', () => { + const scheme1 = TestHelper.mockSchemeWithId(); + scheme1.scope = ScopeTypes.CHANNEL; + + const scheme2 = TestHelper.mockSchemeWithId(); + scheme2.scope = ScopeTypes.TEAM; + + const schemes = {}; + schemes[scheme1.id] = scheme1; + schemes[scheme2.id] = scheme2; + + const channel1 = TestHelper.fakeChannelWithId(); + channel1.scheme_id = scheme1.id; + + const channel2 = TestHelper.fakeChannelWithId(); + + const channels = {}; + channels[channel1.id] = channel1; + channels[channel2.id] = channel2; + + const team1 = TestHelper.fakeTeamWithId(); + team1.scheme_id = scheme2.id; + + const team2 = TestHelper.fakeTeamWithId(); + + const teams = {}; + teams[team1.id] = team1; + teams[team2.id] = team2; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + schemes: {schemes}, + channels: {channels}, + teams: {teams}, + }, + }); + + it('getSchemes', () => { + assert.deepEqual(Selectors.getSchemes(testState), schemes); + }); + + it('getScheme', () => { + assert.equal(Selectors.getScheme(testState, scheme1.id), scheme1); + }); + + it('makeGetSchemeChannels', () => { + const getSchemeChannels = Selectors.makeGetSchemeChannels(); + const results = getSchemeChannels(testState, {schemeId: scheme1.id}); + assert.equal(results.length, 1); + assert.equal(results[0].name, channel1.name); + }); + + it('makeGetSchemeChannels with team scope scheme', () => { + const getSchemeChannels = Selectors.makeGetSchemeChannels(); + const results = getSchemeChannels(testState, {schemeId: scheme2.id}); + assert.equal(results.length, 0); + }); + + it('makeGetSchemeTeams', () => { + const getSchemeTeams = Selectors.makeGetSchemeTeams(); + const results = getSchemeTeams(testState, {schemeId: scheme2.id}); + assert.equal(results.length, 1); + assert.equal(results[0].name, team1.name); + }); + + it('getSchemeTeams with channel scope scheme', () => { + const getSchemeTeams = Selectors.makeGetSchemeTeams(); + const results = getSchemeTeams(testState, {schemeId: scheme1.id}); + assert.equal(results.length, 0); + }); +}); diff --git a/app/mm-redux/selectors/entities/schemes.ts b/app/mm-redux/selectors/entities/schemes.ts new file mode 100644 index 000000000..cda02e7ee --- /dev/null +++ b/app/mm-redux/selectors/entities/schemes.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {ScopeTypes} from '@mm-redux/constants/schemes'; + +import {createSelector} from 'reselect'; +import {getAllChannels} from '@mm-redux/selectors/entities/channels'; +import {getTeams} from '@mm-redux/selectors/entities/teams'; +import {GlobalState} from '@mm-redux/types/store'; +import {Scheme} from '@mm-redux/types/schemes'; +import {Channel} from '@mm-redux/types/channels'; +import {Team} from '@mm-redux/types/teams'; + +export function getSchemes(state: GlobalState): { + [x: string]: Scheme; +} { + return state.entities.schemes.schemes; +} + +export function getScheme(state: GlobalState, id: string): Scheme { + const schemes = getSchemes(state); + return schemes[id]; +} + +export function makeGetSchemeChannels() { + return (createSelector( + getAllChannels, + (state: GlobalState, props: {schemeId: string}) => getScheme(state, props.schemeId), + (allChannels, scheme) => { + if (!scheme) { + return []; + } + + if (scheme.scope === ScopeTypes.TEAM) { + const msg = `Not implemented: scheme '${scheme.id}' is team-scope but 'getSchemeChannels' only accepts channel-scoped schemes.`; + console.log(msg); // eslint-disable-line no-console + return []; + } + + const schemeChannels: Array = []; + + Object.entries(allChannels).forEach((item: [string, Channel]) => { + const [, channel] = item; + if (channel.scheme_id === scheme.id) { + schemeChannels.push(channel); + } + }); + + return schemeChannels; + }) as (b: GlobalState, a: { + schemeId: string; + }) => Array); +} + +export function makeGetSchemeTeams() { + return (createSelector( + getTeams, + (state: GlobalState, props: {schemeId: string}) => getScheme(state, props.schemeId), + (allTeams, scheme) => { + if (!scheme) { + return []; + } + + if (scheme.scope === ScopeTypes.CHANNEL) { + const msg = `Error: scheme '${scheme.id}' is channel-scoped but 'getSchemeChannels' only accepts team-scoped schemes.`; + console.log(msg); // eslint-disable-line no-console + return []; + } + + const schemeTeams: Array = []; + + Object.entries(allTeams).forEach((item: [string, Team]) => { + const [, team] = item; + if (team.scheme_id === scheme.id) { + schemeTeams.push(team); + } + }); + + return schemeTeams; + }) as (b: GlobalState, a: { + schemeId: string; + }) => Array); +} diff --git a/app/mm-redux/selectors/entities/search.test.js b/app/mm-redux/selectors/entities/search.test.js new file mode 100644 index 000000000..1e736ff0d --- /dev/null +++ b/app/mm-redux/selectors/entities/search.test.js @@ -0,0 +1,29 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import TestHelper from 'test/test_helper'; +import * as Selectors from '@mm-redux/selectors/entities/search'; + +describe('Selectors.Search', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const team1CurrentSearch = {params: {page: 0, per_page: 20}, isEnd: true}; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + teams: { + currentTeamId: team1.id, + }, + search: { + current: {[team1.id]: team1CurrentSearch}, + }, + }, + }); + + it('should return current search for current team', () => { + assert.deepEqual(Selectors.getCurrentSearchForCurrentTeam(testState), team1CurrentSearch); + }); +}); diff --git a/app/mm-redux/selectors/entities/search.ts b/app/mm-redux/selectors/entities/search.ts new file mode 100644 index 000000000..097050047 --- /dev/null +++ b/app/mm-redux/selectors/entities/search.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as reselect from 'reselect'; + +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; + +import * as types from '@mm-redux/types'; + +export const getCurrentSearchForCurrentTeam = reselect.createSelector( + (state: types.store.GlobalState) => state.entities.search.current, + getCurrentTeamId, + (current, teamId) => { + return current[teamId]; + } +); diff --git a/app/mm-redux/selectors/entities/teams.test.js b/app/mm-redux/selectors/entities/teams.test.js new file mode 100644 index 000000000..cf09bfa1e --- /dev/null +++ b/app/mm-redux/selectors/entities/teams.test.js @@ -0,0 +1,566 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import TestHelper from 'test/test_helper'; +import * as Selectors from '@mm-redux/selectors/entities/teams'; +import {General} from '../../constants'; + +describe('Selectors.Teams', () => { + TestHelper.initMockEntities(); + const team1 = TestHelper.fakeTeamWithId(); + const team2 = TestHelper.fakeTeamWithId(); + const team3 = TestHelper.fakeTeamWithId(); + const team4 = TestHelper.fakeTeamWithId(); + const team5 = TestHelper.fakeTeamWithId(); + + const teams = {}; + teams[team1.id] = team1; + teams[team2.id] = team2; + teams[team3.id] = team3; + teams[team4.id] = team4; + teams[team5.id] = team5; + team1.display_name = 'Marketeam'; + team1.name = 'marketing_team'; + team2.display_name = 'Core Team'; + team3.allow_open_invite = true; + team4.allow_open_invite = true; + team3.display_name = 'Team AA'; + team4.display_name = 'aa-team'; + team5.delete_at = 10; + team5.allow_open_invite = true; + + const user = TestHelper.fakeUserWithId(); + const user2 = TestHelper.fakeUserWithId(); + const user3 = TestHelper.fakeUserWithId(); + const profiles = {}; + profiles[user.id] = user; + profiles[user2.id] = user2; + profiles[user3.id] = user3; + + const myMembers = {}; + myMembers[team1.id] = {team_id: team1.id, user_id: user.id, roles: General.TEAM_USER_ROLE, mention_count: 1}; + myMembers[team2.id] = {team_id: team2.id, user_id: user.id, roles: General.TEAM_USER_ROLE, mention_count: 3}; + myMembers[team5.id] = {team_id: team5.id, user_id: user.id, roles: General.TEAM_USER_ROLE, mention_count: 0}; + + const membersInTeam = {}; + membersInTeam[team1.id] = {}; + membersInTeam[team1.id][user2.id] = {team_id: team1.id, user_id: user2.id, roles: General.TEAM_USER_ROLE}; + membersInTeam[team1.id][user3.id] = {team_id: team1.id, user_id: user3.id, roles: General.TEAM_USER_ROLE}; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user.id, + profiles, + }, + teams: { + currentTeamId: team1.id, + teams, + myMembers, + membersInTeam, + }, + roles: { + roles: TestHelper.basicRoles, + }, + general: { + serverVersion: '5.8.0', + }, + }, + }); + + it('getTeamsList', () => { + assert.deepEqual(Selectors.getTeamsList(testState), [team1, team2, team3, team4, team5]); + }); + + it('getMyTeams', () => { + assert.deepEqual(Selectors.getMyTeams(testState), [team1, team2]); + }); + + it('getMembersInCurrentTeam', () => { + assert.deepEqual(Selectors.getMembersInCurrentTeam(testState), membersInTeam[team1.id]); + }); + + it('getTeamMember', () => { + assert.deepEqual(Selectors.getTeamMember(testState, team1.id, user2.id), membersInTeam[team1.id][user2.id]); + }); + + it('getJoinableTeams', () => { + const openTeams = [team3, team4]; + const joinableTeams = Selectors.getJoinableTeams(testState); + assert.strictEqual(joinableTeams[0], openTeams[0]); + assert.strictEqual(joinableTeams[1], openTeams[1]); + }); + + it('getSortedJoinableTeams', () => { + const openTeams = [team4, team3]; + const joinableTeams = Selectors.getSortedJoinableTeams(testState); + assert.strictEqual(joinableTeams[0], openTeams[0]); + assert.strictEqual(joinableTeams[1], openTeams[1]); + }); + + it('getListableTeams', () => { + const openTeams = [team3, team4]; + const listableTeams = Selectors.getListableTeams(testState); + assert.strictEqual(listableTeams[0], openTeams[0]); + assert.strictEqual(listableTeams[1], openTeams[1]); + }); + + it('getListedJoinableTeams', () => { + const openTeams = [team4, team3]; + const joinableTeams = Selectors.getSortedListableTeams(testState); + assert.strictEqual(joinableTeams[0], openTeams[0]); + assert.strictEqual(joinableTeams[1], openTeams[1]); + }); + + it('getJoinableTeamsUsingPermissions', () => { + const privateTeams = [team1, team2]; + const openTeams = [team3, team4]; + let modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + ...testState.entities.roles.roles.system_user, + permissions: ['join_private_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + let joinableTeams = Selectors.getJoinableTeams(modifiedState); + assert.strictEqual(joinableTeams[0], privateTeams[0]); + assert.strictEqual(joinableTeams[1], privateTeams[1]); + + modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + permissions: ['join_public_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + joinableTeams = Selectors.getJoinableTeams(modifiedState); + assert.strictEqual(joinableTeams[0], openTeams[0]); + assert.strictEqual(joinableTeams[1], openTeams[1]); + + modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + permissions: ['join_public_teams', 'join_private_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + joinableTeams = Selectors.getJoinableTeams(modifiedState); + assert.strictEqual(joinableTeams[0], privateTeams[0]); + assert.strictEqual(joinableTeams[1], privateTeams[1]); + assert.strictEqual(joinableTeams[2], openTeams[0]); + assert.strictEqual(joinableTeams[3], openTeams[1]); + }); + + it('getSortedJoinableTeamsUsingPermissions', () => { + const privateTeams = [team2, team1]; + const openTeams = [team4, team3]; + const modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + ...testState.entities.roles.roles.system_user, + permissions: ['join_public_teams', 'join_private_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + const joinableTeams = Selectors.getSortedJoinableTeams(modifiedState); + assert.strictEqual(joinableTeams[0], openTeams[0]); + assert.strictEqual(joinableTeams[1], privateTeams[0]); + assert.strictEqual(joinableTeams[2], privateTeams[1]); + assert.strictEqual(joinableTeams[3], openTeams[1]); + }); + + it('getListableTeamsUsingPermissions', () => { + const privateTeams = [team1, team2]; + const openTeams = [team3, team4]; + let modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + ...testState.entities.roles.roles.system_user, + permissions: ['list_private_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + let listableTeams = Selectors.getListableTeams(modifiedState); + assert.strictEqual(listableTeams[0], privateTeams[0]); + assert.strictEqual(listableTeams[1], privateTeams[1]); + + modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + permissions: ['list_public_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + listableTeams = Selectors.getListableTeams(modifiedState); + assert.strictEqual(listableTeams[0], openTeams[0]); + assert.strictEqual(listableTeams[1], openTeams[1]); + + modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + permissions: ['list_public_teams', 'list_private_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + listableTeams = Selectors.getListableTeams(modifiedState); + assert.strictEqual(listableTeams[0], privateTeams[0]); + assert.strictEqual(listableTeams[1], privateTeams[1]); + assert.strictEqual(listableTeams[2], openTeams[0]); + assert.strictEqual(listableTeams[3], openTeams[1]); + }); + + it('getSortedListableTeamsUsingPermissions', () => { + const privateTeams = [team2, team1]; + const openTeams = [team4, team3]; + const modifiedState = { + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: {}, + }, + roles: { + roles: { + system_user: { + ...testState.entities.roles.roles.system_user, + permissions: ['list_public_teams', 'list_private_teams'], + }, + }, + }, + general: { + serverVersion: '5.10.0', + }, + }, + }; + const listableTeams = Selectors.getSortedListableTeams(modifiedState); + assert.strictEqual(listableTeams[0], openTeams[0]); + assert.strictEqual(listableTeams[1], privateTeams[0]); + assert.strictEqual(listableTeams[2], privateTeams[1]); + assert.strictEqual(listableTeams[3], openTeams[1]); + }); + + it('isCurrentUserCurrentTeamAdmin', () => { + assert.deepEqual(Selectors.isCurrentUserCurrentTeamAdmin(testState), false); + }); + + it('getMyTeamMember', () => { + assert.deepEqual(Selectors.getMyTeamMember(testState, team1.id), myMembers[team1.id]); + }); + + it('getTeam', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + teams: { + ...testState.entities.teams.teams, + [team3.id]: { + ...team3, + allow_open_invite: false, + }, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getTeam(testState, team1.id); + const fromModifiedState = Selectors.getTeam(modifiedState, team1.id); + assert.ok(fromOriginalState === fromModifiedState); + }); + + it('getJoinableTeamIds', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + teams: { + ...testState.entities.teams.teams, + [team3.id]: { + ...team3, + display_name: 'Welcome', + }, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getJoinableTeamIds(testState); + const fromModifiedState = Selectors.getJoinableTeamIds(modifiedState); + assert.ok(fromOriginalState === fromModifiedState); + }); + + it('getMySortedTeamIds', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + teams: { + ...testState.entities.teams.teams, + [team3.id]: { + ...team3, + display_name: 'Welcome', + }, + }, + }, + }, + }; + + const updateState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + teams: { + ...testState.entities.teams.teams, + [team2.id]: { + ...team2, + display_name: 'Yankz', + }, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getMySortedTeamIds(testState); + const fromModifiedState = Selectors.getMySortedTeamIds(modifiedState); + const fromUpdateState = Selectors.getMySortedTeamIds(updateState); + + assert.ok(fromOriginalState === fromModifiedState); + assert.ok(fromModifiedState[0] === team2.id); + + assert.ok(fromModifiedState !== fromUpdateState); + assert.ok(fromUpdateState[0] === team1.id); + }); + + it('getMyTeamsCount', () => { + const modifiedState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + teams: { + ...testState.entities.teams.teams, + [team3.id]: { + ...team3, + display_name: 'Welcome', + }, + }, + }, + }, + }; + + const updateState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + myMembers: { + ...testState.entities.teams.myMembers, + [team3.id]: {team_id: team3.id, user_id: user.id, roles: General.TEAM_USER_ROLE}, + }, + }, + }, + }; + + const fromOriginalState = Selectors.getMyTeamsCount(testState); + const fromModifiedState = Selectors.getMyTeamsCount(modifiedState); + const fromUpdateState = Selectors.getMyTeamsCount(updateState); + + assert.ok(fromOriginalState === fromModifiedState); + assert.ok(fromModifiedState === 2); + + assert.ok(fromModifiedState !== fromUpdateState); + assert.ok(fromUpdateState === 3); + }); + + it('getChannelDrawerBadgeCount', () => { + const mentions = Selectors.getChannelDrawerBadgeCount(testState); + assert.ok(mentions === 3); + }); + + it('getTeamMentions', () => { + const factory1 = Selectors.makeGetBadgeCountForTeamId(); + const factory2 = Selectors.makeGetBadgeCountForTeamId(); + const factory3 = Selectors.makeGetBadgeCountForTeamId(); + + const mentions1 = factory1(testState, team1.id); + assert.ok(mentions1 === 1); + + const mentions2 = factory2(testState, team2.id); + assert.ok(mentions2 === 3); + + // Not a member of the team + const mentions3 = factory3(testState, team3.id); + assert.ok(mentions3 === 0); + }); + + it('getCurrentRelativeTeamUrl', () => { + assert.deepEqual(Selectors.getCurrentRelativeTeamUrl(testState), '/' + team1.name); + assert.deepEqual(Selectors.getCurrentRelativeTeamUrl({entities: {teams: {teams: {}}}}), '/'); + }); + + it('getCurrentTeamUrl', () => { + const siteURL = 'http://localhost:8065'; + const general = { + config: {SiteURL: siteURL}, + credentials: {}, + }; + + const withSiteURLState = { + ...testState, + entities: { + ...testState.entities, + general, + }, + }; + withSiteURLState.entities.general = general; + assert.deepEqual(Selectors.getCurrentTeamUrl(withSiteURLState), siteURL + '/' + team1.name); + + const credentialURL = 'http://localhost'; + const withCredentialURLState = { + ...withSiteURLState, + entities: { + ...withSiteURLState.entities, + general: { + ...withSiteURLState.entities.general, + credentials: {url: credentialURL}, + }, + }, + }; + assert.deepEqual(Selectors.getCurrentTeamUrl(withCredentialURLState), credentialURL + '/' + team1.name); + }); + + it('getCurrentTeamUrl with falsy currentTeam', () => { + const siteURL = 'http://localhost:8065'; + const general = { + config: {SiteURL: siteURL}, + credentials: {}, + }; + const falsyCurrentTeamIds = ['', null, undefined]; + falsyCurrentTeamIds.forEach((falsyCurrentTeamId) => { + const withSiteURLState = { + ...testState, + entities: { + ...testState.entities, + teams: { + ...testState.entities.teams, + currentTeamId: falsyCurrentTeamId, + }, + general, + }, + }; + withSiteURLState.entities.general = general; + assert.deepEqual(Selectors.getCurrentTeamUrl(withSiteURLState), siteURL); + + const credentialURL = 'http://localhost'; + const withCredentialURLState = { + ...withSiteURLState, + entities: { + ...withSiteURLState.entities, + general: { + ...withSiteURLState.entities.general, + credentials: {url: credentialURL}, + }, + }, + }; + assert.deepEqual(Selectors.getCurrentTeamUrl(withCredentialURLState), credentialURL); + }); + }); +}); diff --git a/app/mm-redux/selectors/entities/teams.ts b/app/mm-redux/selectors/entities/teams.ts new file mode 100644 index 000000000..ad635fa00 --- /dev/null +++ b/app/mm-redux/selectors/entities/teams.ts @@ -0,0 +1,291 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import * as reselect from 'reselect'; +import {Permissions} from '../../constants'; +import {getConfig, getCurrentUrl, isCompatibleWithJoinViewTeamPermissions} from '@mm-redux/selectors/entities/general'; +import {haveISystemPermission} from '@mm-redux/selectors/entities/roles_helpers'; +import {createIdsSelector} from '@mm-redux/utils/helpers'; +import {isTeamAdmin} from '@mm-redux/utils/user_utils'; +import {sortTeamsWithLocale} from '@mm-redux/utils/team_utils'; +import {Team, TeamMembership} from '@mm-redux/types/teams'; +import {IDMappedObjects} from '@mm-redux/types/utilities'; +import {GlobalState} from '@mm-redux/types/store'; + +export function getCurrentTeamId(state: GlobalState) { + return state.entities.teams.currentTeamId; +} + +export const getTeamByName = reselect.createSelector(getTeams, (state: GlobalState, name: string) => name, (teams: IDMappedObjects, name: string): Team|undefined => { + return Object.values(teams).find((team: Team) => team.name === name); +}); +export function getTeams(state: GlobalState): IDMappedObjects { + return state.entities.teams.teams; +} + +export function getTeamStats(state: GlobalState) { + return state.entities.teams.stats; +} + +export function getTeamMemberships(state: GlobalState) { + return state.entities.teams.myMembers; +} + +export function getMembersInTeams(state: GlobalState) { + return state.entities.teams.membersInTeam; +} + +export const getTeamsList = reselect.createSelector( + getTeams, + (teams) => { + return Object.values(teams); + } +); + +export const getCurrentTeam = reselect.createSelector( + getTeams, + getCurrentTeamId, + (teams, currentTeamId) => { + return teams[currentTeamId]; + } +); + +export function getTeam(state: GlobalState, id: string): Team { + const teams = getTeams(state); + return teams[id]; +} + +export const getCurrentTeamMembership = reselect.createSelector( + getCurrentTeamId, + getTeamMemberships, + (currentTeamId: string, teamMemberships: {[x: string]: TeamMembership}): TeamMembership => { + return teamMemberships[currentTeamId]; + } +); + +export const isCurrentUserCurrentTeamAdmin = reselect.createSelector( + getCurrentTeamMembership, + (member) => { + if (member) { + const roles = member.roles || ''; + return isTeamAdmin(roles); + } + return false; + } +); + +export const getCurrentTeamUrl = reselect.createSelector( + getCurrentUrl, + getCurrentTeam, + (state) => getConfig(state).SiteURL, + (currentURL, currentTeam, siteURL) => { + const rootURL = `${currentURL || siteURL}`; + if (!currentTeam) { + return rootURL; + } + + return `${rootURL}/${currentTeam.name}`; + } +); + +export const getCurrentRelativeTeamUrl = reselect.createSelector( + getCurrentTeam, + (currentTeam) => { + if (!currentTeam) { + return '/'; + } + return `/${currentTeam.name}`; + } +); + +export const getCurrentTeamStats = reselect.createSelector( + getCurrentTeamId, + getTeamStats, + (currentTeamId, teamStats) => { + return teamStats[currentTeamId]; + } +); + +export const getMyTeams = reselect.createSelector( + getTeams, + getTeamMemberships, + (teams, members) => { + return Object.values(teams).filter((t) => members[t.id] && t.delete_at === 0); + } +); + +export const getMyTeamMember = reselect.createSelector( + getTeamMemberships, + (state: GlobalState, teamId: string) => teamId, + (teamMemberships, teamId) => { + return teamMemberships[teamId] || {}; + } +); + +export const getMembersInCurrentTeam = reselect.createSelector( + getCurrentTeamId, + getMembersInTeams, + (currentTeamId, teamMembers) => { + return teamMembers[currentTeamId]; + } +); + +export function getTeamMember(state: GlobalState, teamId: string, userId: string) { + const members = getMembersInTeams(state)[teamId]; + if (members) { + return members[userId]; + } + + return null; +} + +export const getListableTeamIds = createIdsSelector( + getTeams, + getTeamMemberships, + (state) => haveISystemPermission(state, {permission: Permissions.LIST_PUBLIC_TEAMS}), + (state) => haveISystemPermission(state, {permission: Permissions.LIST_PRIVATE_TEAMS}), + isCompatibleWithJoinViewTeamPermissions, + (teams, myMembers, canListPublicTeams, canListPrivateTeams, compatibleWithJoinViewTeamPermissions) => { + return Object.keys(teams).filter((id) => { + const team = teams[id]; + const member = myMembers[id]; + let canList = team.allow_open_invite; + if (compatibleWithJoinViewTeamPermissions) { + canList = (canListPrivateTeams && !team.allow_open_invite) || (canListPublicTeams && team.allow_open_invite); + } + return team.delete_at === 0 && canList && !member; + }); + } +); + +export const getListableTeams = reselect.createSelector( + getTeams, + getListableTeamIds, + (teams, listableTeamIds) => { + return listableTeamIds.map((id) => teams[id]); + } +); + +export const getSortedListableTeams = reselect.createSelector( + getTeams, + getListableTeamIds, + (state: GlobalState, locale: string) => locale, + (teams, listableTeamIds, locale) => { + const listableTeams: {[x: string]: Team} = {}; + + for (const id of listableTeamIds) { + listableTeams[id] = teams[id]; + } + + return Object.values(listableTeams).sort(sortTeamsWithLocale(locale)); + } +); + +export const getJoinableTeamIds = createIdsSelector( + getTeams, + getTeamMemberships, + (state: GlobalState) => haveISystemPermission(state, {permission: Permissions.JOIN_PUBLIC_TEAMS}), + (state: GlobalState) => haveISystemPermission(state, {permission: Permissions.JOIN_PRIVATE_TEAMS}), + isCompatibleWithJoinViewTeamPermissions, + (teams, myMembers, canJoinPublicTeams, canJoinPrivateTeams, compatibleWithJoinViewTeamPermissions) => { + return Object.keys(teams).filter((id) => { + const team = teams[id]; + const member = myMembers[id]; + let canJoin = team.allow_open_invite; + if (compatibleWithJoinViewTeamPermissions) { + canJoin = (canJoinPrivateTeams && !team.allow_open_invite) || (canJoinPublicTeams && team.allow_open_invite); + } + return team.delete_at === 0 && canJoin && !member; + }); + } +); + +export const getJoinableTeams = reselect.createSelector( + getTeams, + getJoinableTeamIds, + (teams, joinableTeamIds) => { + return joinableTeamIds.map((id) => teams[id]); + } +); + +export const getSortedJoinableTeams = reselect.createSelector( + getTeams, + getJoinableTeamIds, + (state: GlobalState, locale: string) => locale, + (teams, joinableTeamIds, locale) => { + const joinableTeams: {[x: string]: Team} = {}; + + for (const id of joinableTeamIds) { + joinableTeams[id] = teams[id]; + } + + return Object.values(joinableTeams).sort(sortTeamsWithLocale(locale)); + } +); + +export const getMySortedTeamIds = createIdsSelector( + getMyTeams, + (state: GlobalState, locale: string) => locale, + (teams, locale) => { + return teams.sort(sortTeamsWithLocale(locale)).map((t) => t.id); + } +); + +export const getMyTeamsCount = reselect.createSelector( + getMyTeams, + (teams) => { + return teams.length; + } +); + +// returns the badge number to show (excluding the current team) +// > 0 means is returning the mention count +// 0 means that there are no unread messages +// -1 means that there are unread messages but no mentions +export const getChannelDrawerBadgeCount = reselect.createSelector( + getCurrentTeamId, + getTeamMemberships, + (currentTeamId, teamMembers) => { + let mentionCount = 0; + let messageCount = 0; + Object.values(teamMembers).forEach((m: TeamMembership) => { + if (m.team_id !== currentTeamId) { + mentionCount += (m.mention_count || 0); + messageCount += (m.msg_count || 0); + } + }); + + let badgeCount = 0; + if (mentionCount) { + badgeCount = mentionCount; + } else if (messageCount) { + badgeCount = -1; + } + + return badgeCount; + } +); + +// returns the badge for a team +// > 0 means is returning the mention count +// 0 means that there are no unread messages +// -1 means that there are unread messages but no mentions +export function makeGetBadgeCountForTeamId() { + return reselect.createSelector( + getTeamMemberships, + (state: GlobalState, id: string) => id, + (members, teamId) => { + const member = members[teamId]; + let badgeCount = 0; + + if (member) { + if (member.mention_count) { + badgeCount = member.mention_count; + } else if (member.msg_count) { + badgeCount = -1; + } + } + + return badgeCount; + } + ); +} diff --git a/app/mm-redux/selectors/entities/timezone.ts b/app/mm-redux/selectors/entities/timezone.ts new file mode 100644 index 000000000..e0c4431a0 --- /dev/null +++ b/app/mm-redux/selectors/entities/timezone.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GlobalState} from '@mm-redux/types/store'; + +export function getUserTimezone(state: GlobalState, id: string) { + const profile = state.entities.users.profiles[id]; + + if (profile && profile.timezone) { + return { + ...profile.timezone, + useAutomaticTimezone: profile.timezone.useAutomaticTimezone === 'true', + }; + } + + return { + useAutomaticTimezone: true, + automaticTimezone: '', + manualTimezone: '', + }; +} + +export function isTimezoneEnabled(state: GlobalState) { + const {config} = state.entities.general; + return config.ExperimentalTimezone === 'true'; +} diff --git a/app/mm-redux/selectors/entities/typing.ts b/app/mm-redux/selectors/entities/typing.ts new file mode 100644 index 000000000..35ea53fce --- /dev/null +++ b/app/mm-redux/selectors/entities/typing.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {getCurrentChannelId, getUsers} from '@mm-redux/selectors/entities/common'; +import {getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences'; +import {displayUsername} from '@mm-redux/utils/user_utils'; +import {Typing} from '@mm-redux/types/typing'; +import {UserProfile} from '@mm-redux/types/users'; +import {GlobalState} from '@mm-redux/types/store'; +import {IDMappedObjects} from '@mm-redux/types/utilities'; + +const getUsersTypingImpl = (profiles: IDMappedObjects, teammateNameDisplay: string, channelId: string, parentPostId: string, typing: Typing): Array => { + const id = channelId + parentPostId; + + if (typing[id]) { + const users = Object.keys(typing[id]); + + if (users.length) { + return users.map((userId) => { + return displayUsername(profiles[userId], teammateNameDisplay); + }); + } + } + + return []; +}; + +export const makeGetUsersTypingByChannelAndPost = () => { + return (createSelector(getUsers, getTeammateNameDisplaySetting, (state: GlobalState, options: { + channelId: string; + postId: string; + }): string => options.channelId, (state: GlobalState, options: { + channelId: string; + postId: string; + }): string => options.postId, (state: GlobalState): Typing => state.entities.typing, getUsersTypingImpl) as (state: GlobalState, a: { + channelId: string; + postId: string; + }) => Array); +}; + +export const getUsersTyping: (state: GlobalState) => Array = createSelector( + getUsers, + getTeammateNameDisplaySetting, + getCurrentChannelId, + (state) => state.entities.posts.selectedPostId, + (state) => state.entities.typing, + getUsersTypingImpl, +); diff --git a/app/mm-redux/selectors/entities/users.test.js b/app/mm-redux/selectors/entities/users.test.js new file mode 100644 index 000000000..470df3ab5 --- /dev/null +++ b/app/mm-redux/selectors/entities/users.test.js @@ -0,0 +1,677 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {General, Preferences} from '../../constants'; +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import {sortByUsername} from '@mm-redux/utils/user_utils'; +import TestHelper from 'test/test_helper'; +import * as Selectors from '@mm-redux/selectors/entities/users'; + +describe('Selectors.Users', () => { + const team1 = TestHelper.fakeTeamWithId(); + + const channel1 = TestHelper.fakeChannelWithId(team1.id); + const channel2 = TestHelper.fakeChannelWithId(team1.id); + + const user1 = TestHelper.fakeUserWithId(); + user1.notify_props = {mention_keys: 'testkey1,testkey2'}; + user1.roles = 'system_admin system_user'; + const user2 = TestHelper.fakeUserWithId(); + user2.delete_at = 1; + const user3 = TestHelper.fakeUserWithId(); + const user4 = TestHelper.fakeUserWithId(); + const user5 = TestHelper.fakeUserWithId(); + const user6 = TestHelper.fakeUserWithId(); + user6.roles = 'system_admin system_user'; + const user7 = TestHelper.fakeUserWithId(); + user7.delete_at = 1; + user7.roles = 'system_admin system_user'; + const profiles = {}; + profiles[user1.id] = user1; + profiles[user2.id] = user2; + profiles[user3.id] = user3; + profiles[user4.id] = user4; + profiles[user5.id] = user5; + profiles[user6.id] = user6; + profiles[user7.id] = user7; + + const profilesInTeam = {}; + profilesInTeam[team1.id] = new Set([user1.id, user2.id, user7.id]); + + const profilesNotInTeam = {}; + profilesNotInTeam[team1.id] = new Set([user3.id, user4.id]); + + const profilesWithoutTeam = new Set([user5.id, user6.id]); + + const profilesInChannel = {}; + profilesInChannel[channel1.id] = new Set([user1.id]); + profilesInChannel[channel2.id] = new Set([user1.id, user2.id]); + + const profilesNotInChannel = {}; + profilesNotInChannel[channel1.id] = new Set([user2.id, user3.id]); + profilesNotInChannel[channel2.id] = new Set([user4.id, user5.id]); + + const userSessions = [{ + create_at: 1, + expires_at: 2, + props: {}, + user_id: user1.id, + roles: '', + }]; + + const userAudits = [{ + action: 'test_user_action', + create_at: 1535007018934, + extra_info: 'success', + id: 'test_id', + ip_address: '::1', + session_id: '', + user_id: 'test_user_id', + }]; + + const myPreferences = {}; + myPreferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${user2.id}`] = {category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: user2.id, value: 'true'}; + myPreferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${user3.id}`] = {category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: user3.id, value: 'false'}; + + const testState = deepFreezeAndThrowOnMutation({ + entities: { + users: { + currentUserId: user1.id, + profiles, + profilesInTeam, + profilesNotInTeam, + profilesWithoutTeam, + profilesInChannel, + profilesNotInChannel, + mySessions: userSessions, + myAudits: userAudits, + }, + teams: { + currentTeamId: team1.id, + }, + channels: { + currentChannelId: channel1.id, + }, + preferences: { + myPreferences, + }, + }, + }); + + it('getUserIdsInChannels', () => { + assert.deepEqual(Selectors.getUserIdsInChannels(testState), profilesInChannel); + }); + + it('getUserIdsNotInChannels', () => { + assert.deepEqual(Selectors.getUserIdsNotInChannels(testState), profilesNotInChannel); + }); + + it('getUserIdsInTeams', () => { + assert.deepEqual(Selectors.getUserIdsInTeams(testState), profilesInTeam); + }); + + it('getUserIdsNotInTeams', () => { + assert.deepEqual(Selectors.getUserIdsNotInTeams(testState), profilesNotInTeam); + }); + + it('getUserIdsWithoutTeam', () => { + assert.deepEqual(Selectors.getUserIdsWithoutTeam(testState), profilesWithoutTeam); + }); + + it('getUserSessions', () => { + assert.deepEqual(Selectors.getUserSessions(testState), userSessions); + }); + + it('getUserAudits', () => { + assert.deepEqual(Selectors.getUserAudits(testState), userAudits); + }); + + it('getUser', () => { + assert.deepEqual(Selectors.getUser(testState, user1.id), user1); + }); + + it('getUsers', () => { + assert.deepEqual(Selectors.getUsers(testState), profiles); + }); + + describe('getCurrentUserMentionKeys', () => { + it('at mention', () => { + const userId = '1234'; + const notifyProps = {}; + const state = { + entities: { + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last', notify_props: notifyProps}, + }, + }, + }, + }; + + assert.deepEqual(Selectors.getCurrentUserMentionKeys(state), [{key: '@user'}]); + }); + + it('channel', () => { + const userId = '1234'; + const notifyProps = { + channel: 'true', + }; + const state = { + entities: { + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last', notify_props: notifyProps}, + }, + }, + }, + }; + + assert.deepEqual(Selectors.getCurrentUserMentionKeys(state), [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@user'}]); + }); + + it('first name', () => { + const userId = '1234'; + const notifyProps = { + first_name: 'true', + }; + const state = { + entities: { + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last', notify_props: notifyProps}, + }, + }, + }, + }; + + assert.deepEqual(Selectors.getCurrentUserMentionKeys(state), [{key: 'First', caseSensitive: true}, {key: '@user'}]); + }); + + it('custom keys', () => { + const userId = '1234'; + const notifyProps = { + mention_keys: 'test,foo,@user,user', + }; + const state = { + entities: { + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last', notify_props: notifyProps}, + }, + }, + }, + }; + + assert.deepEqual(Selectors.getCurrentUserMentionKeys(state), [{key: 'test'}, {key: 'foo'}, {key: '@user'}, {key: 'user'}]); + }); + }); + + describe('getProfiles', () => { + it('getProfiles without filter', () => { + const users = [user1, user2, user3, user4, user5, user6, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfiles(testState), users); + }); + + it('getProfiles with role filter', () => { + const users = [user1, user6, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfiles(testState, {role: 'system_admin'}), users); + }); + it('getProfiles with inactive', () => { + const users = [user2, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfiles(testState, {inactive: true}), users); + }); + it('getProfiles with multiple filters', () => { + const users = [user7]; + assert.deepEqual(Selectors.getProfiles(testState, {role: 'system_admin', inactive: true}), users); + }); + }); + + it('getProfilesInCurrentTeam', () => { + const users = [user1, user2, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfilesInCurrentTeam(testState), users); + }); + describe('getProfilesInTeam', () => { + it('getProfilesInTeam without filter', () => { + const users = [user1, user2, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfilesInTeam(testState, team1.id), users); + assert.deepEqual(Selectors.getProfilesInTeam(testState, 'junk'), []); + }); + it('getProfilesInTeam with role filter', () => { + const users = [user1, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfilesInTeam(testState, team1.id, {role: 'system_admin'}), users); + assert.deepEqual(Selectors.getProfilesInTeam(testState, 'junk', {role: 'system_admin'}), []); + }); + it('getProfilesInTeam with inactive filter', () => { + const users = [user2, user7].sort(sortByUsername); + assert.deepEqual(Selectors.getProfilesInTeam(testState, team1.id, {inactive: true}), users); + assert.deepEqual(Selectors.getProfilesInTeam(testState, 'junk', {inactive: true}), []); + }); + it('getProfilesInTeam with multiple filters', () => { + const users = [user7]; + assert.deepEqual(Selectors.getProfilesInTeam(testState, team1.id, {role: 'system_admin', inactive: true}), users); + }); + }); + + it('getProfilesNotInCurrentTeam', () => { + const users = [user3, user4].sort(sortByUsername); + assert.deepEqual(Selectors.getProfilesNotInCurrentTeam(testState), users); + }); + + describe('getProfilesWithoutTeam', () => { + it('getProfilesWithoutTeam', () => { + const users = [user5, user6].sort(sortByUsername); + assert.deepEqual(Selectors.getProfilesWithoutTeam(testState), users); + }); + it('getProfilesWithoutTeam with filter', () => { + assert.deepEqual(Selectors.getProfilesWithoutTeam(testState, {role: 'system_admin'}), [user6]); + }); + }); + + describe('searchProfiles', () => { + it('searchProfiles without filter', () => { + assert.deepEqual(Selectors.searchProfiles(testState, user1.username), [user1]); + assert.deepEqual(Selectors.searchProfiles(testState, user2.first_name + ' ' + user2.last_name), [user2]); + assert.deepEqual(Selectors.searchProfiles(testState, user1.username, true), []); + }); + + it('searchProfiles with filters', () => { + assert.deepEqual(Selectors.searchProfiles(testState, user1.username, false, {role: 'system_admin'}), [user1]); + assert.deepEqual(Selectors.searchProfiles(testState, user3.username, false, {role: 'system_admin'}), []); + assert.deepEqual(Selectors.searchProfiles(testState, user3.username, false, {inactive: true}), []); + assert.deepEqual(Selectors.searchProfiles(testState, user2.username, false, {inactive: true}), [user2]); + }); + }); + + it('searchProfilesInCurrentChannel', () => { + assert.deepEqual(Selectors.searchProfilesInCurrentChannel(testState, user1.username), [user1]); + assert.deepEqual(Selectors.searchProfilesInCurrentChannel(testState, user1.username, true), []); + }); + + it('searchProfilesNotInCurrentChannel', () => { + assert.deepEqual(Selectors.searchProfilesNotInCurrentChannel(testState, user2.username), [user2]); + assert.deepEqual(Selectors.searchProfilesNotInCurrentChannel(testState, user2.username, true), [user2]); + }); + + it('searchProfilesInCurrentTeam', () => { + assert.deepEqual(Selectors.searchProfilesInCurrentTeam(testState, user1.username), [user1]); + assert.deepEqual(Selectors.searchProfilesInCurrentTeam(testState, user1.username, true), []); + }); + + describe('searchProfilesInTeam', () => { + it('searchProfilesInTeam without filter', () => { + assert.deepEqual(Selectors.searchProfilesInTeam(testState, team1.id, user1.username), [user1]); + assert.deepEqual(Selectors.searchProfilesInTeam(testState, team1.id, user1.username, true), []); + }); + it('searchProfilesInTeam with filter', () => { + assert.deepEqual(Selectors.searchProfilesInTeam(testState, team1.id, user1.username, false, {role: 'system_admin'}), [user1]); + assert.deepEqual(Selectors.searchProfilesInTeam(testState, team1.id, user1.username, false, {inactive: true}), []); + }); + it('getProfiles with multiple filters', () => { + const users = [user7]; + assert.deepEqual(Selectors.searchProfilesInTeam(testState, team1.id, user7.username, false, {role: 'system_admin', inactive: true}), users); + }); + }); + + it('searchProfilesNotInCurrentTeam', () => { + assert.deepEqual(Selectors.searchProfilesNotInCurrentTeam(testState, user3.username), [user3]); + assert.deepEqual(Selectors.searchProfilesNotInCurrentTeam(testState, user3.username, true), [user3]); + }); + + describe('searchProfilesWithoutTeam', () => { + it('searchProfilesWithoutTeam without filter', () => { + assert.deepEqual(Selectors.searchProfilesWithoutTeam(testState, user5.username), [user5]); + assert.deepEqual(Selectors.searchProfilesWithoutTeam(testState, user5.username, true), [user5]); + }); + it('searchProfilesWithoutTeam with filter', () => { + assert.deepEqual(Selectors.searchProfilesWithoutTeam(testState, user6.username, false, {role: 'system_admin'}), [user6]); + assert.deepEqual(Selectors.searchProfilesWithoutTeam(testState, user5.username, false, {inactive: true}), []); + }); + }); + it('isCurrentUserSystemAdmin', () => { + assert.deepEqual(Selectors.isCurrentUserSystemAdmin(testState), true); + }); + + it('getUserByUsername', () => { + assert.deepEqual(Selectors.getUserByUsername(testState, user1.username), user1); + }); + + it('getUsersInVisibleDMs', () => { + assert.deepEqual(Selectors.getUsersInVisibleDMs(testState), [user2]); + }); + + it('getUserByEmail', () => { + assert.deepEqual(Selectors.getUserByEmail(testState, user1.email), user1); + assert.deepEqual(Selectors.getUserByEmail(testState, user2.email), user2); + }); + + it('makeGetProfilesInChannel', () => { + const getProfilesInChannel = Selectors.makeGetProfilesInChannel(); + assert.deepEqual(getProfilesInChannel(testState, channel1.id), [user1]); + + const users = [user1, user2].sort(sortByUsername); + assert.deepEqual(getProfilesInChannel(testState, channel2.id), users); + assert.deepEqual(getProfilesInChannel(testState, channel2.id, true), [user1]); + + assert.deepEqual(getProfilesInChannel(testState, 'nonexistentid'), []); + assert.deepEqual(getProfilesInChannel(testState, 'nonexistentid'), []); + }); + + it('makeGetProfilesInChannel, unknown user id in channel', () => { + const state = { + ...testState, + entities: { + ...testState.entities, + users: { + ...testState.entities.users, + profilesInChannel: { + ...testState.entities.users.profilesInChannel, + [channel1.id]: new Set([...testState.entities.users.profilesInChannel[channel1.id], 'unknown']), + }, + }, + }, + }; + + const getProfilesInChannel = Selectors.makeGetProfilesInChannel(); + assert.deepEqual(getProfilesInChannel(state, channel1.id), [user1]); + assert.deepEqual(getProfilesInChannel(state, channel1.id, true), [user1]); + }); + + it('makeGetProfilesNotInChannel', () => { + const getProfilesNotInChannel = Selectors.makeGetProfilesNotInChannel(); + + assert.deepEqual(getProfilesNotInChannel(testState, channel1.id, true), [user3].sort(sortByUsername)); + assert.deepEqual(getProfilesNotInChannel(testState, channel1.id), [user2, user3].sort(sortByUsername)); + + assert.deepEqual(getProfilesNotInChannel(testState, channel2.id, true), [user4, user5].sort(sortByUsername)); + assert.deepEqual(getProfilesNotInChannel(testState, channel2.id), [user4, user5].sort(sortByUsername)); + + assert.deepEqual(getProfilesNotInChannel(testState, 'nonexistentid'), []); + assert.deepEqual(getProfilesNotInChannel(testState, 'nonexistentid'), []); + }); + + it('makeGetProfilesByIdsAndUsernames', () => { + const getProfilesByIdsAndUsernames = Selectors.makeGetProfilesByIdsAndUsernames(); + + const testCases = [ + {input: {allUserIds: [], allUsernames: []}, output: []}, + {input: {allUserIds: ['nonexistentid'], allUsernames: ['nonexistentid']}, output: []}, + {input: {allUserIds: [user1.id], allUsernames: []}, output: [user1]}, + {input: {allUserIds: [user1.id]}, output: [user1]}, + {input: {allUserIds: [user1.id, 'nonexistentid']}, output: [user1]}, + {input: {allUserIds: [user1.id, user2.id]}, output: [user1, user2]}, + {input: {allUserIds: ['nonexistentid', user1.id, user2.id]}, output: [user1, user2]}, + {input: {allUserIds: [], allUsernames: [user1.username]}, output: [user1]}, + {input: {allUsernames: [user1.username]}, output: [user1]}, + {input: {allUsernames: [user1.username, 'nonexistentid']}, output: [user1]}, + {input: {allUsernames: [user1.username, user2.username]}, output: [user1, user2]}, + {input: {allUsernames: [user1.username, 'nonexistentid', user2.username]}, output: [user1, user2]}, + {input: {allUserIds: [user1.id], allUsernames: [user2.username]}, output: [user1, user2]}, + {input: {allUserIds: [user1.id, user2.id], allUsernames: [user3.username, user4.username]}, output: [user1, user2, user3, user4]}, + {input: {allUserIds: [user1.username, user2.username], allUsernames: [user3.id, user4.id]}, output: []}, + ]; + + testCases.forEach((testCase) => { + assert.deepEqual(getProfilesByIdsAndUsernames(testState, testCase.input), testCase.output); + }); + }); + + describe('makeGetDisplayName', () => { + const testUser1 = { + ...user1, + id: 'test_user_id', + username: 'username', + first_name: 'First', + last_name: 'Last', + }; + const newProfiles = { + ...profiles, + [testUser1.id]: testUser1, + }; + it('Should show full name since preferences is being used and LockTeammateNameDisplay is false', () => { + const newTestState = { + entities: { + users: {profiles: newProfiles}, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.NAME_NAME_FORMAT}`]: { + value: General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME, + }, + }, + }, + general: { + config: { + TeammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME, + LockTeammateNameDisplay: 'false', + }, + license: { + LockTeammateNameDisplay: 'true', + }, + }, + }, + }; + assert.deepEqual(Selectors.makeGetDisplayName()(newTestState, testUser1.id), 'First Last'); + }); + it('Should show show username since LockTeammateNameDisplay is true', () => { + const newTestState = { + entities: { + users: {profiles: newProfiles}, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.NAME_NAME_FORMAT}`]: { + value: General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME, + }, + }, + }, + general: { + config: { + TeammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME, + LockTeammateNameDisplay: 'true', + }, + license: { + LockTeammateNameDisplay: 'true', + }, + }, + }, + }; + assert.deepEqual(Selectors.makeGetDisplayName()(newTestState, testUser1.id), 'username'); + }); + it('Should show full name since license is false', () => { + const newTestState = { + entities: { + users: {profiles: newProfiles}, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.NAME_NAME_FORMAT}`]: { + value: General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME, + }, + }, + }, + general: { + config: { + TeammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME, + LockTeammateNameDisplay: 'true', + }, + license: { + LockTeammateNameDisplay: 'false', + }, + }, + }, + }; + assert.deepEqual(Selectors.makeGetDisplayName()(newTestState, testUser1.id), 'First Last'); + }); + it('Should show full name since license is not available', () => { + const newTestState = { + entities: { + users: {profiles: newProfiles}, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.NAME_NAME_FORMAT}`]: { + value: General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME, + }, + }, + }, + general: { + config: { + TeammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME, + LockTeammateNameDisplay: 'true', + }, + }, + }, + }; + assert.deepEqual(Selectors.makeGetDisplayName()(newTestState, testUser1.id), 'First Last'); + }); + it('Should show Full name since license is not available and lock teammate name display is false', () => { + const newTestState = { + entities: { + users: {profiles: newProfiles}, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.NAME_NAME_FORMAT}`]: { + value: General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME, + }, + }, + }, + general: { + config: { + TeammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME, + LockTeammateNameDisplay: 'false', + }, + }, + }, + }; + assert.deepEqual(Selectors.makeGetDisplayName()(newTestState, testUser1.id), 'First Last'); + }); + it('Should show username since no settings are available (falls back to default)', () => { + const newTestState = { + entities: { + users: {profiles: newProfiles}, + preferences: { + myPreferences: { + [`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.NAME_NAME_FORMAT}`]: { + }, + }, + }, + general: { + config: { + }, + }, + }, + }; + assert.deepEqual(Selectors.makeGetDisplayName()(newTestState, testUser1.id), 'username'); + }); + }); + + it('shouldShowTermsOfService', () => { + const userId = 1234; + + // Test latest terms not accepted + assert.equal(Selectors.shouldShowTermsOfService({ + entities: { + general: { + config: { + CustomTermsOfServiceId: '1', + EnableCustomTermsOfService: 'true', + }, + license: { + IsLicensed: 'true', + }, + }, + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last'}, + }, + }, + }, + }), true); + + // Test Feature disabled + assert.equal(Selectors.shouldShowTermsOfService({ + entities: { + general: { + config: { + CustomTermsOfServiceId: '1', + EnableCustomTermsOfService: 'false', + }, + license: { + IsLicensed: 'true', + }, + }, + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last'}, + }, + }, + }, + }), false); + + // Test unlicensed + assert.equal(Selectors.shouldShowTermsOfService({ + entities: { + general: { + config: { + CustomTermsOfServiceId: '1', + EnableCustomTermsOfService: 'true', + }, + license: { + IsLicensed: 'false', + }, + }, + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last'}, + }, + }, + }, + }), false); + + // Test terms already accepted + assert.equal(Selectors.shouldShowTermsOfService({ + entities: { + general: { + config: { + CustomTermsOfServiceId: '1', + EnableCustomTermsOfService: 'true', + }, + license: { + IsLicensed: 'true', + }, + }, + users: { + currentUserId: userId, + profiles: { + [userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last', terms_of_service_id: '1', terms_of_service_create_at: new Date().getTime()}, + }, + }, + }, + }), false); + + // Test not logged in + assert.equal(Selectors.shouldShowTermsOfService({ + entities: { + general: { + config: { + CustomTermsOfServiceId: '1', + EnableCustomTermsOfService: 'true', + }, + license: { + IsLicensed: 'true', + }, + }, + users: { + currentUserId: userId, + profiles: {}, + }, + }, + }), false); + }); +}); + diff --git a/app/mm-redux/selectors/entities/users.ts b/app/mm-redux/selectors/entities/users.ts new file mode 100644 index 000000000..86605119b --- /dev/null +++ b/app/mm-redux/selectors/entities/users.ts @@ -0,0 +1,500 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {createSelector} from 'reselect'; +import {getCurrentChannelId, getCurrentUser, getCurrentUserId, getMyCurrentChannelMembership, getUsers} from '@mm-redux/selectors/entities/common'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; +import {getDirectShowPreferences, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences'; +import {displayUsername, filterProfilesMatchingTerm, sortByUsername, isSystemAdmin, profileListToMap} from '@mm-redux/utils/user_utils'; +export {getCurrentUserId, getCurrentUser, getUsers}; +import {GlobalState} from '@mm-redux/types/store'; +import {UserProfile} from '@mm-redux/types/users'; +import {Reaction} from '@mm-redux/types/reactions'; +import {Team} from '@mm-redux/types/teams'; +import {Channel} from '@mm-redux/types/channels'; +import {RelationOneToOne, RelationOneToMany, IDMappedObjects, UsernameMappedObjects, EmailMappedObjects, $ID, $Username, $Email, Dictionary} from '@mm-redux/types/utilities'; +type Filters = { + role?: string; + inactive?: boolean; +}; +export function getUserIdsInChannels(state: GlobalState): RelationOneToMany { + return state.entities.users.profilesInChannel; +} + +export function getUserIdsNotInChannels(state: GlobalState): RelationOneToMany { + return state.entities.users.profilesNotInChannel; +} + +export function getUserIdsInTeams(state: GlobalState): RelationOneToMany { + return state.entities.users.profilesInTeam; +} + +export function getUserIdsNotInTeams(state: GlobalState): RelationOneToMany { + return state.entities.users.profilesNotInTeam; +} + +export function getUserIdsWithoutTeam(state: GlobalState): Set<$ID> { + return state.entities.users.profilesWithoutTeam; +} + +export function getUserStatuses(state: GlobalState): RelationOneToOne { + return state.entities.users.statuses; +} + +export function getUserSessions(state: GlobalState): Array { + return state.entities.users.mySessions; +} + +export function getUserAudits(state: GlobalState): Array { + return state.entities.users.myAudits; +} + +export function getUser(state: GlobalState, id: $ID): UserProfile { + return state.entities.users.profiles[id]; +} + +export const getUsersByUsername: (a: GlobalState) => UsernameMappedObjects = createSelector( + getUsers, + (users) => { + const usersByUsername: Dictionary = {}; + + for (const id in users) { + if (users.hasOwnProperty(id)) { + const user = users[id]; + usersByUsername[user.username] = user; + } + } + + return usersByUsername; + } +); + +export function getUserByUsername(state: GlobalState, username: $Username): UserProfile { + return getUsersByUsername(state)[username]; +} + +export const getUsersByEmail: (a: GlobalState) => EmailMappedObjects = createSelector( + getUsers, + (users) => { + const usersByEmail: Dictionary = {}; + + for (const user of Object.keys(users).map((key) => users[key])) { + usersByEmail[user.email] = user; + } + + return usersByEmail; + } +); + +export function getUserByEmail(state: GlobalState, email: $Email): UserProfile { + return getUsersByEmail(state)[email]; +} + +export const isCurrentUserSystemAdmin: (a: GlobalState) => boolean = createSelector( + getCurrentUser, + (user) => { + const roles = user.roles || ''; + return isSystemAdmin(roles); + } +); + +export const getCurrentUserRoles: (a: GlobalState) => UserProfile['roles'] = createSelector(getMyCurrentChannelMembership, (state) => state.entities.teams.myMembers[state.entities.teams.currentTeamId], getCurrentUser, (currentChannelMembership, currentTeamMembership, currentUser) => { + let roles = ''; + if (currentTeamMembership) { + roles += `${currentTeamMembership.roles} `; + } + + if (currentChannelMembership) { + roles += `${currentChannelMembership.roles} `; + } + + if (currentUser) { + roles += currentUser.roles; + } + return roles.trim(); +} +); + +export type UserMentionKey= { + key: string; + caseSensitive?: boolean; +} + +export const getCurrentUserMentionKeys: (a: GlobalState) => Array = createSelector(getCurrentUser, (user: UserProfile) => { + let keys: UserMentionKey[] = []; + + if (!user || !user.notify_props) { + return keys; + } + + if (user.notify_props.mention_keys) { + keys = keys.concat(user.notify_props.mention_keys.split(',').map((key) => { + return {key}; + })); + } + + if (user.notify_props.first_name === 'true' && user.first_name) { + keys.push({key: user.first_name, caseSensitive: true}); + } + + if (user.notify_props.channel === 'true') { + keys.push({key: '@channel'}); + keys.push({key: '@all'}); + keys.push({key: '@here'}); + } + + const usernameKey = '@' + user.username; + if (keys.findIndex((key) => key.key === usernameKey) === -1) { + keys.push({key: usernameKey}); + } + + return keys; +} +); + +export const getProfileSetInCurrentChannel: (a: GlobalState) => Array<$ID> = createSelector( + getCurrentChannelId, + getUserIdsInChannels, + (currentChannel, channelProfiles) => { + return channelProfiles[currentChannel]; + } +); + +export const getProfileSetNotInCurrentChannel: (a: GlobalState) => Array<$ID> = createSelector( + getCurrentChannelId, + getUserIdsNotInChannels, + (currentChannel, channelProfiles) => { + return channelProfiles[currentChannel]; + } +); + +export const getProfileSetInCurrentTeam: (a: GlobalState) => Array<$ID> = createSelector( + (state) => state.entities.teams.currentTeamId, + getUserIdsInTeams, + (currentTeam, teamProfiles) => { + return teamProfiles[currentTeam]; + } +); + +export const getProfileSetNotInCurrentTeam: (a: GlobalState) => Array<$ID> = createSelector( + (state) => state.entities.teams.currentTeamId, + getUserIdsNotInTeams, + (currentTeam, teamProfiles) => { + return teamProfiles[currentTeam]; + } +); + +const PROFILE_SET_ALL = 'all'; +function sortAndInjectProfiles(profiles: IDMappedObjects, profileSet?: 'all' | Array<$ID> | Set<$ID>, skipInactive = false): Array { + let currentProfiles: UserProfile[] = []; + + if (typeof profileSet === 'undefined') { + return currentProfiles; + } else if (profileSet === PROFILE_SET_ALL) { + currentProfiles = Object.keys(profiles).map((key) => profiles[key]); + } else { + currentProfiles = Array.from(profileSet).map((p) => profiles[p]); + } + + currentProfiles = currentProfiles.filter((profile) => Boolean(profile)); + + if (skipInactive) { + currentProfiles = currentProfiles.filter((profile) => !(profile.delete_at && profile.delete_at !== 0)); + } + + return currentProfiles.sort(sortByUsername); +} + +export const getProfiles: (a: GlobalState, b: Filters) => Array = createSelector( + getUsers, + (state: GlobalState, filters: Filters) => filters, + (profiles, filters) => { + return sortAndInjectProfiles(filterProfiles(profiles, filters), PROFILE_SET_ALL); + } +); + +function filterProfiles(profiles: IDMappedObjects, filters?: Filters): IDMappedObjects { + if (!filters) { + return profiles; + } + + let users = Object.keys(profiles).map((key) => profiles[key]); + + if (filters.role && filters.role !== '') { + users = users.filter((user) => user.roles && user.roles.includes((filters && filters.role) || '')); + } + + if (filters.inactive) { + users = users.filter((user) => user.delete_at !== 0); + } + + return users.reduce((acc, user) => { + acc[user.id] = user; + return acc; + }, {} as IDMappedObjects); +} + +export function getIsManualStatusForUserId(state: GlobalState, userId: $ID): boolean { + return state.entities.users.isManualStatus[userId]; +} + +export const getProfilesInCurrentChannel: (a: GlobalState) => Array = createSelector( + getUsers, + getProfileSetInCurrentChannel, + (profiles, currentChannelProfileSet) => { + return sortAndInjectProfiles(profiles, currentChannelProfileSet); + } +); + +export const getProfilesNotInCurrentChannel: (a: GlobalState) => Array = createSelector( + getUsers, + getProfileSetNotInCurrentChannel, + (profiles, notInCurrentChannelProfileSet) => { + return sortAndInjectProfiles(profiles, notInCurrentChannelProfileSet); + } +); + +export const getProfilesInCurrentTeam: (a: GlobalState) => Array = createSelector( + getUsers, + getProfileSetInCurrentTeam, + (profiles, currentTeamProfileSet) => { + return sortAndInjectProfiles(profiles, currentTeamProfileSet); + } +); + +export const getProfilesInTeam: (a: GlobalState, b: $ID) => Array = createSelector( + getUsers, + getUserIdsInTeams, + (state: GlobalState, teamId: string) => teamId, + (state: GlobalState, teamId, filters) => filters, + (profiles, usersInTeams, teamId, filters) => { + return sortAndInjectProfiles(filterProfiles(profiles, filters), usersInTeams[teamId] || new Set()); + } +); + +export const getProfilesNotInCurrentTeam: (a: GlobalState) => Array = createSelector( + getUsers, + getProfileSetNotInCurrentTeam, + (profiles, notInCurrentTeamProfileSet) => { + return sortAndInjectProfiles(profiles, notInCurrentTeamProfileSet); + } +); + +export const getProfilesWithoutTeam: (a: GlobalState, filters?: Filters) => Array = createSelector( + getUsers, + getUserIdsWithoutTeam, + (state: GlobalState, filters: Filters) => filters, + (profiles, withoutTeamProfileSet, filters) => { + return sortAndInjectProfiles(filterProfiles(profiles, filters), withoutTeamProfileSet); + } +); +export function getStatusForUserId(state: GlobalState, userId: $ID): string { + return getUserStatuses(state)[userId]; +} + +export function getTotalUsersStats(state: GlobalState): any { + return state.entities.users.stats; +} + +export function searchProfiles(state: GlobalState, term: string, skipCurrent = false, filters?: Filters): Array { + const users = getUsers(state); + const profiles = filterProfilesMatchingTerm(Object.keys(users).map((key) => users[key]), term); + const filteredProfilesMap = filterProfiles(profileListToMap(profiles), filters); + const filteredProfiles = Object.keys(filteredProfilesMap).map((key) => filteredProfilesMap[key]); + + if (skipCurrent) { + removeCurrentUserFromList(filteredProfiles, getCurrentUserId(state)); + } + + return filteredProfiles; +} + +export function searchProfilesInCurrentChannel(state: GlobalState, term: string, skipCurrent = false): Array { + const profiles = filterProfilesMatchingTerm(getProfilesInCurrentChannel(state), term); + + if (skipCurrent) { + removeCurrentUserFromList(profiles, getCurrentUserId(state)); + } + + return profiles; +} + +export function searchProfilesNotInCurrentChannel(state: GlobalState, term: string, skipCurrent = false): Array { + const profiles = filterProfilesMatchingTerm(getProfilesNotInCurrentChannel(state), term); + if (skipCurrent) { + removeCurrentUserFromList(profiles, getCurrentUserId(state)); + } + + return profiles; +} + +export function searchProfilesInCurrentTeam(state: GlobalState, term: string, skipCurrent = false): Array { + const profiles = filterProfilesMatchingTerm(getProfilesInCurrentTeam(state), term); + if (skipCurrent) { + removeCurrentUserFromList(profiles, getCurrentUserId(state)); + } + + return profiles; +} + +export function searchProfilesInTeam(state: GlobalState, teamId: $ID, term: string, skipCurrent = false, filters?: Filters): Array { + const profiles = filterProfilesMatchingTerm(getProfilesInTeam(state, teamId), term); + const filteredProfilesMap = filterProfiles(profileListToMap(profiles), filters); + const filteredProfiles = Object.keys(filteredProfilesMap).map((key) => filteredProfilesMap[key]); + if (skipCurrent) { + removeCurrentUserFromList(filteredProfiles, getCurrentUserId(state)); + } + + return filteredProfiles; +} + +export function searchProfilesNotInCurrentTeam(state: GlobalState, term: string, skipCurrent = false): Array { + const profiles = filterProfilesMatchingTerm(getProfilesNotInCurrentTeam(state), term); + if (skipCurrent) { + removeCurrentUserFromList(profiles, getCurrentUserId(state)); + } + + return profiles; +} + +export function searchProfilesWithoutTeam(state: GlobalState, term: string, skipCurrent = false, filters?: Filters): Array { + const filteredProfiles = filterProfilesMatchingTerm(getProfilesWithoutTeam(state, filters), term); + if (skipCurrent) { + removeCurrentUserFromList(filteredProfiles, getCurrentUserId(state)); + } + + return filteredProfiles; +} + +function removeCurrentUserFromList(profiles: Array, currentUserId: $ID) { + const index = profiles.findIndex((p) => p.id === currentUserId); + if (index >= 0) { + profiles.splice(index, 1); + } +} + +export const shouldShowTermsOfService: (a: GlobalState) => boolean = createSelector( + getConfig, + getCurrentUser, + getLicense, + (config, user, license) => { + // Defaults to false if the user is not logged in or the setting doesn't exist + const acceptedTermsId = user ? user.terms_of_service_id : ''; + const acceptedAt = user ? user.terms_of_service_create_at : 0; + + const featureEnabled = license.IsLicensed === 'true' && config.EnableCustomTermsOfService === 'true'; + const reacceptanceTime = parseInt(config.CustomTermsOfServiceReAcceptancePeriod!, 10) * 1000 * 60 * 60 * 24; + const timeElapsed = new Date().getTime() - acceptedAt; + return Boolean(user && featureEnabled && (config.CustomTermsOfServiceId !== acceptedTermsId || timeElapsed > reacceptanceTime)); + } +); + +export const getUsersInVisibleDMs: (a: GlobalState) => Array = createSelector( + getUsers, + getDirectShowPreferences, + (users, preferences) => { + const dmUsers: UserProfile[] = []; + preferences.forEach((pref) => { + if (pref.value === 'true' && users[pref.name]) { + dmUsers.push(users[pref.name]); + } + }); + return dmUsers; + } +); + +export function makeGetProfilesForReactions(): (a: GlobalState, b: Array) => Array { + return createSelector( + getUsers, + (state: GlobalState, reactions: Array) => reactions, + (users, reactions) => { + const profiles: UserProfile[] = []; + reactions.forEach((r) => { + if (users[r.user_id]) { + profiles.push(users[r.user_id]); + } + }); + return profiles; + } + ); +} + +export function makeGetProfilesInChannel(): (a: GlobalState, b: $ID, c: boolean) => Array { + return createSelector( + getUsers, + getUserIdsInChannels, + (state: GlobalState, channelId: string) => channelId, + (state, channelId, skipInactive) => skipInactive, + (users, userIds, channelId, skipInactive = false) => { + const userIdsInChannel = userIds[channelId]; + + if (!userIdsInChannel) { + return []; + } + + return sortAndInjectProfiles(users, userIdsInChannel, skipInactive); + } + ); +} + +export function makeGetProfilesNotInChannel(): (a: GlobalState, b: $ID, c: boolean) => Array { + return createSelector( + getUsers, + getUserIdsNotInChannels, + (state: GlobalState, channelId: string) => channelId, + (state, channelId, skipInactive) => skipInactive, + (users, userIds, channelId, skipInactive = false) => { + const userIdsInChannel = userIds[channelId]; + + if (!userIdsInChannel) { + return []; + } + + return sortAndInjectProfiles(users, userIdsInChannel, skipInactive); + } + ); +} + +export function makeGetProfilesByIdsAndUsernames(): (a: GlobalState, b: {allUserIds: Array<$ID>; allUsernames: Array<$Username>}) => Array { + return createSelector( + getUsers, + getUsersByUsername, + (state: GlobalState, props: {allUserIds: Array<$ID>; allUsernames: Array<$Username>}) => props.allUserIds, + (state, props) => props.allUsernames, + (allProfilesById: Dictionary, allProfilesByUsername: Dictionary, allUserIds: Array, allUsernames: Array) => { + const userProfiles: UserProfile[] = []; + + if (allUserIds && allUserIds.length > 0) { + const profilesById = allUserIds. + filter((userId) => allProfilesById[userId]). + map((userId) => allProfilesById[userId]); + + if (profilesById && profilesById.length > 0) { + userProfiles.push(...profilesById); + } + } + + if (allUsernames && allUsernames.length > 0) { + const profilesByUsername = allUsernames. + filter((username) => allProfilesByUsername[username]). + map((username) => allProfilesByUsername[username]); + + if (profilesByUsername && profilesByUsername.length > 0) { + userProfiles.push(...profilesByUsername); + } + } + + return userProfiles; + } + ); +} + +export function makeGetDisplayName(): (a: GlobalState, b: $ID, c: boolean) => string { + return createSelector( + (state: GlobalState, userId: string) => getUser(state, userId), + getTeammateNameDisplaySetting, + (state, _, useFallbackUsername = true) => useFallbackUsername, + (user, teammateNameDisplaySetting, useFallbackUsername) => { + return displayUsername(user, teammateNameDisplaySetting!, useFallbackUsername); + } + ); +} diff --git a/app/mm-redux/selectors/errors.ts b/app/mm-redux/selectors/errors.ts new file mode 100644 index 000000000..4fb091326 --- /dev/null +++ b/app/mm-redux/selectors/errors.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GlobalState} from '@mm-redux/types/store'; + +export function getDisplayableErrors(state: GlobalState) { + return state.errors.filter((error) => error.displayable); +} diff --git a/app/mm-redux/selectors/index.ts b/app/mm-redux/selectors/index.ts new file mode 100644 index 000000000..2a2f93334 --- /dev/null +++ b/app/mm-redux/selectors/index.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as entities from './entities'; +import * as errors from './errors'; + +export {errors, entities}; diff --git a/app/mm-redux/store/configureStore.dev.ts b/app/mm-redux/store/configureStore.dev.ts new file mode 100644 index 000000000..66adff4f6 --- /dev/null +++ b/app/mm-redux/store/configureStore.dev.ts @@ -0,0 +1,102 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-undefined */ +import * as redux from 'redux'; +import {createOfflineReducer, networkStatusChangedAction, offlineCompose} from 'redux-offline'; +import defaultOfflineConfig from 'redux-offline/lib/defaults'; +import reducerRegistry from './reducer_registry'; +import devTools from 'remote-redux-devtools'; + +const globalAny = global as any; +const window = globalAny.window; + +const devToolsEnhancer = typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__ ? // eslint-disable-line no-underscore-dangle + window.__REDUX_DEVTOOLS_EXTENSION__ : // eslint-disable-line no-underscore-dangle + () => { + return devTools({ + name: 'Mattermost', + hostname: 'localhost', + port: 5678, + realtime: true, + }); + }; +import serviceReducer from '../reducers'; +import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import initialState from './initial_state'; +import {offlineConfig, createReducer} from './helpers'; +import {createMiddleware} from './middleware'; +import {Reducer, Action} from '@mm-redux/types/actions'; +import {GlobalState} from '@mm-redux/types/store'; + +/** + * Configures and constructs the redux store. Accepts the following parameters: + * preloadedState - Any preloaded state to be applied to the store after it is initially configured. + * appReducer - An object containing any app-specific reducer functions that the client needs. + * userOfflineConfig - Any additional configuration data to be passed into redux-offline aside from the default values. + * getAppReducer - A function that returns the appReducer as defined above. Only used in development to enable hot reloading. + * clientOptions - An object containing additional options used when configuring the redux store. The following options are available: + * additionalMiddleware - func | array - Allows for single or multiple additional middleware functions to be passed in from the client side. + * enableBuffer - bool - default = true - If true, the store will buffer all actions until offline state rehydration occurs. + * enableThunk - bool - default = true - If true, include the thunk middleware automatically. If false, thunk must be provided as part of additionalMiddleware. + */ +export default function configureServiceStore(preloadedState: any, appReducer: any, userOfflineConfig: any, getAppReducer: any, clientOptions: any) { + const baseOfflineConfig = Object.assign({}, defaultOfflineConfig, offlineConfig, userOfflineConfig); + const baseState = Object.assign({}, initialState, preloadedState); + + const loadReduxDevtools = process.env.NODE_ENV !== 'test'; //eslint-disable-line no-process-env + + const store = redux.createStore( + createOfflineReducer(createDevReducer(baseState, serviceReducer, appReducer)), + baseState, + offlineCompose(baseOfflineConfig)( + createMiddleware(clientOptions), + loadReduxDevtools ? [devToolsEnhancer()] : [] + ) + ); + + reducerRegistry.setChangeListener((reducers: any) => { + store.replaceReducer(createOfflineReducer(createDevReducer(baseState, reducers))); + }); + + // launch store persistor + if (baseOfflineConfig.persist) { + baseOfflineConfig.persist(store, baseOfflineConfig.persistOptions, baseOfflineConfig.persistCallback); + } + + if (baseOfflineConfig.detectNetwork) { + baseOfflineConfig.detectNetwork((online: boolean) => { + store.dispatch(networkStatusChangedAction(online)); + }); + } + + if ((module as any).hot) { + // Enable Webpack hot module replacement for reducers + (module as any).hot.accept(() => { + const nextServiceReducer = require('../reducers').default; // eslint-disable-line global-require + let nextAppReducer; + if (getAppReducer) { + nextAppReducer = getAppReducer(); // eslint-disable-line global-require + } + store.replaceReducer(createDevReducer(baseState, reducerRegistry.getReducers(), nextServiceReducer, nextAppReducer)); + }); + } + + return store; +} + +function createDevReducer(baseState: any, ...reducers: any) { + return enableFreezing(createReducer(baseState, ...reducers)); +} + +function enableFreezing(reducer: Reducer) { + return (state: GlobalState, action: Action) => { + const nextState = reducer(state, action); + + if (nextState !== state) { + deepFreezeAndThrowOnMutation(nextState); + } + + return nextState; + }; +} diff --git a/app/mm-redux/store/configureStore.prod.ts b/app/mm-redux/store/configureStore.prod.ts new file mode 100644 index 000000000..f1d456386 --- /dev/null +++ b/app/mm-redux/store/configureStore.prod.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as redux from 'redux'; +import reducerRegistry from './reducer_registry'; + +import serviceReducer from '../reducers'; + +import {offlineConfig, createReducer} from './helpers'; +import initialState from './initial_state'; +import {createMiddleware} from './middleware'; + +import {createOfflineReducer, networkStatusChangedAction, offlineCompose} from 'redux-offline'; +import defaultOfflineConfig from 'redux-offline/lib/defaults'; + +/** + * Configures and constructs the redux store. Accepts the following parameters: + * preloadedState - Any preloaded state to be applied to the store after it is initially configured. + * appReducer - An object containing any app-specific reducer functions that the client needs. + * userOfflineConfig - Any additional configuration data to be passed into redux-offline aside from the default values. + * getAppReducer - A function that returns the appReducer as defined above. Only used in development to enable hot reloading. + * clientOptions - An object containing additional options used when configuring the redux store. The following options are available: + * additionalMiddleware - func | array - Allows for single or multiple additional middleware functions to be passed in from the client side. + * enableBuffer - bool - default = true - If true, the store will buffer all actions until offline state rehydration occurs. + * enableThunk - bool - default = true - If true, include the thunk middleware automatically. If false, thunk must be provided as part of additionalMiddleware. + */ +export default function configureOfflineServiceStore(preloadedState: any, appReducer: any, userOfflineConfig: any, getAppReducer: any, clientOptions = {}) { + const baseState = Object.assign({}, initialState, preloadedState); + + const baseOfflineConfig = Object.assign({}, defaultOfflineConfig, offlineConfig, userOfflineConfig); + + const store = redux.createStore( + createOfflineReducer(createReducer(baseState, serviceReducer as any, appReducer)), + baseState, + offlineCompose(baseOfflineConfig)( + createMiddleware(clientOptions), + [] + ) + ); + + reducerRegistry.setChangeListener((reducers: any) => { + store.replaceReducer(createOfflineReducer(createReducer(baseState, reducers))); + }); + + // launch store persistor + if (baseOfflineConfig.persist) { + baseOfflineConfig.persist(store, baseOfflineConfig.persistOptions, baseOfflineConfig.persistCallback); + } + + if (baseOfflineConfig.detectNetwork) { + baseOfflineConfig.detectNetwork((online: boolean) => { + store.dispatch(networkStatusChangedAction(online)); + }); + } + + return store; +} diff --git a/app/mm-redux/store/helpers.ts b/app/mm-redux/store/helpers.ts new file mode 100644 index 000000000..4904ffec6 --- /dev/null +++ b/app/mm-redux/store/helpers.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {combineReducers} from 'redux'; +import {General} from '../constants'; +import reducerRegistry from './reducer_registry'; +import {enableBatching, Action, Reducer} from '@mm-redux/types/actions'; +export const offlineConfig = { + effect: (effect: Function, action: Action) => { + if (typeof effect !== 'function') { + throw new Error('Offline Action: effect must be a function.'); + } else if (!('meta' in action && action.meta && action.meta.offline.commit)) { + throw new Error('Offline Action: commit action must be present.'); + } + + return effect(); + }, + discard: (error: Error, action: Action, retries: number) => { + if ('meta' in action && action.meta && action.meta.offline.hasOwnProperty('maxRetry')) { + return retries >= action.meta.offline.maxRetry; + } + + return retries > 10; + }, +}; + +export function createReducer(baseState: any, ...reducers: Reducer[]) { + reducerRegistry.setReducers(Object.assign({}, ...reducers)); + const baseReducer = combineReducers(reducerRegistry.getReducers()); + + // Root reducer wrapper that listens for reset events. + // Returns whatever is passed for the data property + // as the new state. + function offlineReducer(state = {}, action: Action) { + if ('type' in action && 'data' in action && action.type === General.OFFLINE_STORE_RESET) { + return baseReducer(action.data || baseState, action); + } + + return baseReducer(state, action as any); + } + + return enableBatching(offlineReducer); +} diff --git a/app/mm-redux/store/index.ts b/app/mm-redux/store/index.ts new file mode 100644 index 000000000..2c2a0e8d3 --- /dev/null +++ b/app/mm-redux/store/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable global-require, no-process-env */ +const config = process.env.NODE_ENV === 'production' ? require('./configureStore.prod').default : require('./configureStore.dev').default; +export default config; diff --git a/app/mm-redux/store/initial_state.ts b/app/mm-redux/store/initial_state.ts new file mode 100644 index 000000000..efac8c741 --- /dev/null +++ b/app/mm-redux/store/initial_state.ts @@ -0,0 +1,270 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GlobalState} from '@mm-redux/types/store'; + +const state: GlobalState = { + entities: { + general: { + appState: false, + credentials: {}, + config: {}, + dataRetentionPolicy: {}, + deviceToken: '', + license: {}, + serverVersion: '', + timezones: [], + }, + users: { + currentUserId: '', + isManualStatus: {}, + mySessions: [], + myAudits: [], + profiles: {}, + profilesInTeam: {}, + profilesNotInTeam: {}, + profilesWithoutTeam: new Set(), + profilesInChannel: {}, + profilesNotInChannel: {}, + statuses: {}, + stats: {}, + }, + teams: { + currentTeamId: '', + teams: {}, + myMembers: {}, + membersInTeam: {}, + stats: {}, + groupsAssociatedToTeam: {}, + totalCount: 0, + }, + channels: { + currentChannelId: '', + channels: {}, + channelsInTeam: {}, + myMembers: {}, + membersInChannel: {}, + stats: {}, + groupsAssociatedToChannel: {}, + totalCount: 0, + manuallyUnread: {}, + channelModerations: {}, + }, + posts: { + expandedURLs: {}, + posts: {}, + postsInChannel: {}, + postsInThread: {}, + pendingPostIds: [], + reactions: {}, + openGraph: {}, + selectedPostId: '', + currentFocusedPostId: '', + messagesHistory: { + messages: [], + index: { + post: -1, + comment: -1, + }, + }, + }, + preferences: { + myPreferences: {}, + }, + bots: { + accounts: {}, + }, + jobs: { + jobs: {}, + jobsByTypeList: {}, + }, + integrations: { + incomingHooks: {}, + outgoingHooks: {}, + oauthApps: {}, + systemCommands: {}, + commands: {}, + }, + files: { + files: {}, + fileIdsByPostId: {}, + }, + emojis: { + customEmoji: {}, + nonExistentEmoji: new Set(), + }, + search: { + results: [], + current: {}, + recent: {}, + matches: {}, + flagged: [], + pinned: {}, + isSearchingTerm: false, + isSearchGettingMore: false, + }, + typing: {}, + roles: { + roles: {}, + pending: new Set(), + }, + gifs: { + categories: { + tagsList: [], + tagsDict: {}, + }, + cache: { + gifs: {}, + updating: false, + }, + search: { + searchText: '', + searchBarText: '', + resultsByTerm: {}, + scrollPosition: 0, + priorLocation: null, + }, + }, + schemes: { + schemes: {}, + }, + groups: { + groups: {}, + syncables: {}, + members: {}, + }, + channelCategories: { + byId: {}, + orderByTeam: {}, + }, + }, + errors: [], + requests: { + channels: { + getAllChannels: { + status: 'not_started', + error: null, + }, + getChannels: { + status: 'not_started', + error: null, + }, + myChannels: { + status: 'not_started', + error: null, + }, + createChannel: { + status: 'not_started', + error: null, + }, + updateChannel: { + status: 'not_started', + error: null, + }, + }, + general: { + websocket: { + status: 'not_started', + error: null, + }, + }, + posts: { + createPost: { + status: 'not_started', + error: null, + }, + editPost: { + status: 'not_started', + error: null, + }, + getPostThread: { + status: 'not_started', + error: null, + }, + }, + teams: { + getMyTeams: { + status: 'not_started', + error: null, + }, + getTeams: { + status: 'not_started', + error: null, + }, + joinTeam: { + status: 'not_started', + error: null, + }, + }, + users: { + checkMfa: { + status: 'not_started', + error: null, + }, + login: { + status: 'not_started', + error: null, + }, + logout: { + status: 'not_started', + error: null, + }, + autocompleteUsers: { + status: 'not_started', + error: null, + }, + updateMe: { + status: 'not_started', + error: null, + }, + }, + files: { + uploadFiles: { + status: 'not_started', + error: null, + }, + }, + roles: { + getRolesByNames: { + status: 'not_started', + error: null, + }, + getRoleByName: { + status: 'not_started', + error: null, + }, + getRole: { + status: 'not_started', + error: null, + }, + editRole: { + status: 'not_started', + error: null, + }, + }, + jobs: { + createJob: { + status: 'not_started', + error: null, + }, + getJob: { + status: 'not_started', + error: null, + }, + getJobs: { + status: 'not_started', + error: null, + }, + cancelJob: { + status: 'not_started', + error: null, + }, + }, + }, + websocket: { + connected: false, + lastConnectAt: 0, + lastDisconnectAt: 0, + }, +}; +export default state; diff --git a/app/mm-redux/store/middleware.ts b/app/mm-redux/store/middleware.ts new file mode 100644 index 000000000..88a1ca420 --- /dev/null +++ b/app/mm-redux/store/middleware.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import thunk, {ThunkMiddleware} from 'redux-thunk'; + +import createActionBuffer from 'redux-action-buffer'; +import {REHYDRATE} from 'redux-persist/constants'; + +const defaultOptions = { + additionalMiddleware: [], + enableBuffer: true, + enableThunk: true, +}; +export function createMiddleware(clientOptions: any): ThunkMiddleware[] { + const options = Object.assign({}, defaultOptions, clientOptions); + const { + additionalMiddleware, + enableBuffer, + enableThunk, + } = options; + const middleware: ThunkMiddleware[] = []; + + if (enableThunk) { + middleware.push(thunk); + } + + if (additionalMiddleware) { + if (typeof additionalMiddleware === 'function') { + middleware.push(additionalMiddleware); + } else { + middleware.push(...additionalMiddleware); + } + } + + if (enableBuffer) { + middleware.push(createActionBuffer(REHYDRATE)); + } + + return middleware; +} diff --git a/app/mm-redux/store/reducer_registry.test.js b/app/mm-redux/store/reducer_registry.test.js new file mode 100644 index 000000000..bd8ac9017 --- /dev/null +++ b/app/mm-redux/store/reducer_registry.test.js @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import reducerRegistry from '@mm-redux/store/reducer_registry'; +import configureStore from 'test/test_store'; + +describe('ReducerRegistry', () => { + let store; + + function testReducer() { + return 'teststate'; + } + + beforeEach(async () => { + store = await configureStore(); + }); + + it('register reducer', () => { + reducerRegistry.register('testReducer', testReducer); + assert.equal(store.getState().testReducer, 'teststate'); + }); + + it('get reducers', () => { + reducerRegistry.register('testReducer', testReducer); + const reducers = reducerRegistry.getReducers(); + assert.ok(reducers.testReducer); + assert.ok(reducers.entities); + assert.ok(reducers.requests); + assert.ok(reducers.errors); + }); +}); + diff --git a/app/mm-redux/store/reducer_registry.ts b/app/mm-redux/store/reducer_registry.ts new file mode 100644 index 000000000..f046e4d07 --- /dev/null +++ b/app/mm-redux/store/reducer_registry.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Reducer} from '@mm-redux/types/actions'; +import {Dictionary} from '@mm-redux/types/utilities'; + +// Based on http://nicolasgallagher.com/redux-modules-and-code-splitting/ +export class ReducerRegistry { + emitChange?: Function; + reducers: Dictionary = {}; + + setReducers = (reducers: Dictionary) => { + this.reducers = reducers; + } + + getReducers = () => { + return {...this.reducers}; + } + + register = (name: string, reducer: Reducer) => { + this.reducers = {...this.reducers, [name]: reducer}; + if (this.emitChange) { + this.emitChange(this.getReducers()); + } + } + + setChangeListener = (listener: Function) => { + this.emitChange = listener; + } +} + +const reducerRegistry = new ReducerRegistry(); +export default reducerRegistry; diff --git a/app/mm-redux/types/actions.ts b/app/mm-redux/types/actions.ts new file mode 100644 index 000000000..29b1c3c8d --- /dev/null +++ b/app/mm-redux/types/actions.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {GlobalState} from './store'; + +export type GetStateFunc = () => GlobalState; +export type GenericAction = { + type: string; + data?: any; + meta?: any; + error?: any; + index?: number; + displayable?: boolean; + postId?: string; + sessionId?: string; + currentUserId?: string; + remove?: Function|string[]; + timestamp?: number; + [extraProps: string]: any; +}; +export type Thunk = (b: DispatchFunc, a: GetStateFunc) => Promise | ActionResult; + +type BatchAction = { + type: 'BATCHING_REDUCER.BATCH'; + payload: Array; + meta: { + batch: true; + }; +}; +export type Action = GenericAction | Thunk | BatchAction | ActionFunc; + +export type ActionResult = { + data: any; +} | { + error: any; +}; + +export type DispatchFunc = (action: Action, getState?: GetStateFunc | null) => Promise; +export type ActionFunc = (dispatch: DispatchFunc, getState: GetStateFunc) => Promise | ActionResult; +export type PlatformType = 'web' | 'ios' | 'android'; + +export const BATCH = 'BATCHING_REDUCER.BATCH'; + +export function batchActions(actions: Action[], type = BATCH) { + return {type, meta: {batch: true}, payload: actions}; +} + +export type Reducer = ( + state: S | undefined, + action: A + ) => S + +export function enableBatching(reduce: Reducer): Reducer { + return function batchingReducer(state, action) { + if (action && 'meta' in action && action.meta.batch) { + return action.payload.reduce(batchingReducer, state); + } + return reduce(state, action); + }; +} diff --git a/app/mm-redux/types/alerts.ts b/app/mm-redux/types/alerts.ts new file mode 100644 index 000000000..3e8c05091 --- /dev/null +++ b/app/mm-redux/types/alerts.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type AlertTypeType = 'notification' | 'developer' | 'error'; +export type AlertType = { + type: AlertTypeType; + message: string; +}; diff --git a/app/mm-redux/types/bots.ts b/app/mm-redux/types/bots.ts new file mode 100644 index 000000000..2a3e17c2a --- /dev/null +++ b/app/mm-redux/types/bots.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type Bot = { + user_id: string ; + username: string ; + display_name?: string ; + description?: string ; + owner_id: string ; + create_at: number ; + update_at: number ; + delete_at: number ; +} + +// BotPatch is a description of what fields to update on an existing bot. +export type BotPatch = { + username: string; + display_name: string; + description: string; +} \ No newline at end of file diff --git a/app/mm-redux/types/channel_categories.ts b/app/mm-redux/types/channel_categories.ts new file mode 100644 index 000000000..2aa50d10e --- /dev/null +++ b/app/mm-redux/types/channel_categories.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {$ID, IDMappedObjects, RelationOneToOne} from './utilities'; +import {Team} from './teams'; + +export type ChannelCategoryType = 'favorites' | 'public' | 'private' | 'direct_messages' | 'custom'; + +export type ChannelCategory = { + id: string; + team_id: $ID; + type: ChannelCategoryType; + display_name: string; + + // This will be added in phase 2 of Channel Sidebar Organization once the server provides the categories + // channel_ids: $ID; +}; + +export type ChannelCategoriesState = { + byId: IDMappedObjects; + orderByTeam: RelationOneToOne[]>; +}; diff --git a/app/mm-redux/types/channels.ts b/app/mm-redux/types/channels.ts new file mode 100644 index 000000000..d72489b22 --- /dev/null +++ b/app/mm-redux/types/channels.ts @@ -0,0 +1,100 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {IDMappedObjects, UserIDMappedObjects, RelationOneToMany, RelationOneToOne} from './utilities'; +import {Team} from './teams'; +export type ChannelType = 'O' | 'P' | 'D' | 'G'; +export type ChannelStats = { + channel_id: string; + member_count: number; + pinnedpost_count: number; +}; +export type ChannelNotifyProps = { + desktop: 'default' | 'all' | 'mention' | 'none'; + email: 'default' | 'all' | 'mention' | 'none'; + mark_unread: 'all' | 'mention'; + push: 'default' | 'all' | 'mention' | 'none'; + ignore_channel_mentions: 'default' | 'off' | 'on'; +}; +export type Channel = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + team_id: string; + type: ChannelType; + display_name: string; + name: string; + header: string; + purpose: string; + last_post_at: number; + total_msg_count: number; + extra_update_at: number; + creator_id: string; + scheme_id: string; + isCurrent?: boolean; + teammate_id?: string; + status?: string; + fake?: boolean; + group_constrained: boolean; +}; +export type ChannelWithTeamData = Channel & { + team_display_name: string; + team_name: string; + team_update_at: number; +} +export type ChannelMembership = { + channel_id: string; + user_id: string; + roles: string; + last_viewed_at: number; + msg_count: number; + mention_count: number; + notify_props: Partial; + last_update_at: number; + scheme_user: boolean; + scheme_admin: boolean; + post_root_id?: string; +}; +export type ChannelUnread = { + channel_id: string; + user_id: string; + team_id: string; + msg_count: number; + mention_count: number; + last_viewed_at: number; + deltaMsgs: number; +}; +export type ChannelsState = { + currentChannelId: string; + channels: IDMappedObjects; + channelsInTeam: RelationOneToMany; + myMembers: RelationOneToOne; + membersInChannel: RelationOneToOne>; + stats: RelationOneToOne; + groupsAssociatedToChannel: any; + totalCount: number; + manuallyUnread: RelationOneToOne; + channelModerations: RelationOneToOne>; +}; + +export type ChannelModeration = { + name: string; + roles: { + guests?: { + value: boolean; + enabled: boolean; + }; + members: { + value: boolean; + enabled: boolean; + }; + }; +}; + +export type ChannelModerationPatch = { + name: string; + roles: { + guests?: boolean; + members?: boolean; + }; +}; diff --git a/app/mm-redux/types/client4.ts b/app/mm-redux/types/client4.ts new file mode 100644 index 000000000..fcee74987 --- /dev/null +++ b/app/mm-redux/types/client4.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// I assume these are the loglevels +export type logLevel = 'ERROR' | 'WARNING' | 'INFO'; +export type GenericClientResponse = { + response: any; + headers: Map; + data: any; +}; +type ErrorOffline = { + message: string; + url: string; +}; +type ErrorInvalidResponse = { + intl: { + id: string; + defaultMessage: string; + }; +}; +export type ErrorApi = { + message: string; + server_error_id: string; + status_code: number; + url: string; +}; +export type Client4Error = ErrorOffline | ErrorInvalidResponse | ErrorApi; +export type Options = { + headers?: { [x: string]: string }; + method?: string; + url?: string; + credentials?: 'omit' | 'same-origin' | 'include'; + body?: any; +}; diff --git a/app/mm-redux/types/config.ts b/app/mm-redux/types/config.ts new file mode 100644 index 000000000..c13b9f3ed --- /dev/null +++ b/app/mm-redux/types/config.ts @@ -0,0 +1,165 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type Config = { + AboutLink: string; + AllowBannerDismissal: string; + AllowCustomThemes: string; + AllowedThemes: string; + AndroidAppDownloadLink: string; + AndroidLatestVersion: string; + AndroidMinVersion: string; + AppDownloadLink: string; + AsymmetricSigningPublicKey: string; + AvailableLocales: string; + BannerColor: string; + BannerText: string; + BannerTextColor: string; + BuildDate: string; + BuildEnterpriseReady: string; + BuildHash: string; + BuildHashEnterprise: string; + BuildNumber: string; + CloseUnusedDirectMessages: string; + CustomBrandText: string; + CustomDescriptionText: string; + CustomTermsOfServiceId: string; + CustomTermsOfServiceReAcceptancePeriod: string; + CustomUrlSchemes: string; + DataRetentionEnableFileDeletion: string; + DataRetentionEnableMessageDeletion: string; + DataRetentionFileRetentionDays: string; + DataRetentionMessageRetentionDays: string; + DefaultClientLocale: string; + DefaultTheme: string; + DesktopLatestVersion: string; + DesktopMinVersion: string; + DiagnosticId: string; + DiagnosticsEnabled: string; + EmailLoginButtonBorderColor: string; + EmailLoginButtonColor: string; + EmailLoginButtonTextColor: string; + EmailNotificationContentsType: string; + EnableBanner: string; + EnableBotAccountCreation: string; + EnableChannelViewedMessages: string; + EnableCluster: string; + EnableCommands: string; + EnableCompliance: string; + EnableConfirmNotificationsToChannel: string; + EnableCustomBrand: string; + EnableCustomEmoji: string; + EnableCustomTermsOfService: string; + EnableDeveloper: string; + EnableDiagnostics: string; + EnableEmailBatching: string; + EnableEmailInvitations: string; + EnableEmojiPicker: string; + EnableFileAttachments: string; + EnableGifPicker: string; + EnableGuestAccounts: string; + EnableIncomingWebhooks: string; + EnableLatex: string; + EnableLdap: string; + EnableLinkPreviews: string; + EnableMarketplace: string; + EnableMetrics: string; + EnableMobileFileDownload: string; + EnableMobileFileUpload: string; + EnableMultifactorAuthentication: string; + EnableOAuthServiceProvider: string; + EnableOpenServer: string; + EnableOutgoingWebhooks: string; + EnablePostIconOverride: string; + EnablePostUsernameOverride: string; + EnablePreviewFeatures: string; + EnablePreviewModeBanner: string; + EnablePublicLink: string; + EnableSaml: string; + EnableSignInWithEmail: string; + EnableSignInWithUsername: string; + EnableSignUpWithEmail: string; + EnableSignUpWithGitLab: string; + EnableSignUpWithGoogle: string; + EnableSignUpWithOffice365: string; + EnableSVGs: string; + EnableTesting: string; + EnableThemeSelection: string; + EnableTutorial: string; + EnableUserAccessTokens: string; + EnableUserCreation: string; + EnableUserDeactivation: string; + EnableUserTypingMessages: string; + EnableXToLeaveChannelsFromLHS: string; + EnforceMultifactorAuthentication: string; + ExperimentalChannelOrganization: string; + ExperimentalChannelSidebarOrganization: string; + ExperimentalClientSideCertCheck: string; + ExperimentalClientSideCertEnable: string; + ExperimentalEnableAuthenticationTransfer: string; + ExperimentalEnableAutomaticReplies: string; + ExperimentalEnableClickToReply: string; + ExperimentalEnableDefaultChannelLeaveJoinMessages: string; + ExperimentalEnablePostMetadata: string; + ExperimentalGroupUnreadChannels: string; + ExperimentalHideTownSquareinLHS: string; + ExperimentalPrimaryTeam: string; + ExperimentalTimezone: string; + ExperimentalTownSquareIsReadOnly: string; + ExperimentalViewArchivedChannels: string; + GfycatApiKey: string; + GfycatApiSecret: string; + GoogleDeveloperKey: string; + GuestAccountsEnforceMultifactorAuthentication: string; + HasImageProxy: string; + HelpLink: string; + IosAppDownloadLink: string; + IosLatestVersion: string; + IosMinVersion: string; + LdapFirstNameAttributeSet: string; + LdapLastNameAttributeSet: string; + LdapLoginButtonBorderColor: string; + LdapLoginButtonColor: string; + LdapLoginButtonTextColor: string; + LdapLoginFieldName: string; + LdapNicknameAttributeSet: string; + LdapPositionAttributeSet: string; + LockTeammateNameDisplay: string; + MaxFileSize: string; + MaxNotificationsPerChannel: string; + MinimumHashtagLength: string; + PasswordMinimumLength: string; + PasswordRequireLowercase: string; + PasswordRequireNumber: string; + PasswordRequireSymbol: string; + PasswordRequireUppercase: string; + PluginsEnabled: string; + PostEditTimeLimit: string; + PrivacyPolicyLink: string; + ReportAProblemLink: string; + RequireEmailVerification: string; + RestrictDirectMessage: string; + RunJobs: string; + SamlFirstNameAttributeSet: string; + SamlLastNameAttributeSet: string; + SamlLoginButtonBorderColor: string; + SamlLoginButtonColor: string; + SamlLoginButtonText: string; + SamlLoginButtonTextColor: string; + SamlNicknameAttributeSet: string; + SamlPositionAttributeSet: string; + SendEmailNotifications: string; + SendPushNotifications: string; + ShowEmailAddress: string; + SiteName: string; + SiteURL: string; + SQLDriverName: string; + SupportEmail: string; + TeammateNameDisplay: string; + TermsOfServiceLink: string; + TimeBetweenUserTypingUpdatesMilliseconds: string; + Version: string; + WebsocketPort: string; + WebsocketSecurePort: string; + WebsocketURL: string; +}; diff --git a/app/mm-redux/types/emojis.ts b/app/mm-redux/types/emojis.ts new file mode 100644 index 000000000..4c94879cf --- /dev/null +++ b/app/mm-redux/types/emojis.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type EmojiCategory = ( + | 'recent' + | 'people' + | 'nature' + | 'foods' + | 'activity' + | 'places' + | 'objects' + | 'symbols' + | 'flags' + | 'custom' +); +export type CustomEmoji = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + creator_id: string; + name: string; + category: 'custom'; +}; +export type SystemEmoji = { + filename: string; + aliases: Array; + category: EmojiCategory; + batch: number; +}; +export type Emoji = SystemEmoji | CustomEmoji; +export type EmojisState = { + customEmoji: { + [x: string]: CustomEmoji; + }; + nonExistentEmoji: Set; +}; diff --git a/app/mm-redux/types/errors.ts b/app/mm-redux/types/errors.ts new file mode 100644 index 000000000..7003f4bbe --- /dev/null +++ b/app/mm-redux/types/errors.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type Error = { + server_error_id?: string; + stack?: string; + message: string; + status_code?: number; +}; diff --git a/app/mm-redux/types/files.ts b/app/mm-redux/types/files.ts new file mode 100644 index 000000000..38cf1a639 --- /dev/null +++ b/app/mm-redux/types/files.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Dictionary} from './utilities'; + +export type FileInfo = { + id: string; + user_id: string; + create_at: number; + update_at: number; + delete_at: number; + name: string; + extension: string; + size: number; + mime_type: string; + width: number; + height: number; + has_preview_image: boolean; + clientId: string; +}; +export type FilesState = { + files: Dictionary; + fileIdsByPostId: Dictionary>; + filePublicLink?: string; +}; + +export type FileUploadResponse = { + file_infos: FileInfo[]; + client_ids: string[]; +} \ No newline at end of file diff --git a/app/mm-redux/types/general.ts b/app/mm-redux/types/general.ts new file mode 100644 index 000000000..c51e0b7de --- /dev/null +++ b/app/mm-redux/types/general.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Config} from './config'; + +export type GeneralState = { + appState: boolean; + credentials: any; + config: Partial; + dataRetentionPolicy: any; + deviceToken: string; + license: any; + serverVersion: string; + timezones: Array; +}; diff --git a/app/mm-redux/types/groups.ts b/app/mm-redux/types/groups.ts new file mode 100644 index 000000000..f361a944f --- /dev/null +++ b/app/mm-redux/types/groups.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type SyncableType = 'team' | 'channel'; +export type SyncablePatch = { + scheme_admin: boolean; + auto_add: boolean; +}; +export type Group = { + id: string; + name: string; + display_name: string; + description: string; + type: string; + remote_id: string; + create_at: number; + update_at: number; + delete_at: number; + has_syncables: boolean; + member_count: number; + scheme_admin: boolean; +}; +export type GroupTeam = { + team_id: string; + team_display_name: string; + team_type: string; + group_id: string; + auto_add: boolean; + scheme_admin: boolean; + create_at: number; + delete_at: number; + update_at: number; +}; +export type GroupChannel = { + channel_id: string; + channel_display_name: string; + channel_type: string; + team_id: string; + team_display_name: string; + team_type: string; + group_id: string; + auto_add: boolean; + scheme_admin: boolean; + create_at: number; + delete_at: number; + update_at: number; +}; +export type GroupSyncables = { + teams: Array; + channels: Array; +}; +export type GroupsState = { + syncables: { + [x: string]: GroupSyncables; + }; + members: any; + groups: { + [x: string]: Group; + }; +}; +export type GroupSearchOpts = { + q: string; + is_linked?: boolean; + is_configured?: boolean; +}; diff --git a/app/mm-redux/types/index.ts b/app/mm-redux/types/index.ts new file mode 100644 index 000000000..5e33439ec --- /dev/null +++ b/app/mm-redux/types/index.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as actions from './actions'; +import * as channels from './channels'; +import * as client4 from './client4'; +import * as errors from './errors'; +import * as emojis from './emojis'; +import * as files from './files'; +import * as general from './general'; +import * as groups from './groups'; +import * as integrations from './integrations'; +import * as jobs from './jobs'; +import * as posts from './posts'; +import * as preferences from './preferences'; +import * as reactions from './reactions'; +import * as requests from './requests'; +import * as roles from './roles'; +import * as schemes from './schemes'; +import * as search from './search'; +import * as store from './store'; +import * as teams from './teams'; +import * as typing from './typing'; +import * as utilities from './utilities'; +import * as users from './users'; +import * as bots from './bots'; +import * as plugins from './plugins'; +import * as config from './config'; + +export { + config, + actions, + bots, + plugins, + store, + channels, + errors, + emojis, + files, + general, + groups, + typing, + integrations, + jobs, + utilities, + posts, + preferences, + roles, + client4, + schemes, + search, + teams, + requests, + reactions, + users, +}; diff --git a/app/mm-redux/types/integrations.ts b/app/mm-redux/types/integrations.ts new file mode 100644 index 000000000..92dd8755e --- /dev/null +++ b/app/mm-redux/types/integrations.ts @@ -0,0 +1,103 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {IDMappedObjects} from './utilities'; +export type IncomingWebhook = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + user_id: string; + channel_id: string; + team_id: string; + display_name: string; + description: string; + username: string; + icon_url: string; + channel_locked: boolean; +}; +export type OutgoingWebhook = { + id: string; + token: string; + create_at: number; + update_at: number; + delete_at: number; + creator_id: string; + channel_id: string; + team_id: string; + trigger_words: Array; + trigger_when: number; + callback_urls: Array; + display_name: string; + description: string; + content_type: string; + username: string; + icon_url: string; +}; +export type Command = { + 'id': string; + 'token': string; + 'create_at': number; + 'update_at': number; + 'delete_at': number; + 'creator_id': string; + 'team_id': string; + 'trigger': string; + 'method': 'P' | 'G' | ''; + 'username': string; + 'icon_url': string; + 'auto_complete': boolean; + 'auto_complete_desc': string; + 'auto_complete_hint': string; + 'display_name': string; + 'description': string; + 'url': string; +}; +export type OAuthApp = { + 'id': string; + 'creator_id': string; + 'create_at': number; + 'update_at': number; + 'client_secret': string; + 'name': string; + 'description': string; + 'icon_url': string; + 'callback_urls': Array; + 'homepage': string; + 'is_trusted': boolean; +}; +export type IntegrationsState = { + incomingHooks: IDMappedObjects; + outgoingHooks: IDMappedObjects; + oauthApps: IDMappedObjects; + systemCommands: IDMappedObjects; + commands: IDMappedObjects; +}; +export type DialogSubmission = { + url: string; + callback_id: string; + state: string; + user_id: string; + channel_id: string; + team_id: string; + submission: { + [x: string]: string; + }; + cancelled: boolean; +}; +export type DialogElement = { + display_name: string; + name: string; + type: string; + subtype: string; + default: string; + placeholder: string; + help_text: string; + optional: boolean; + min_length: number; + max_length: number; + data_source: string; + options: Array<{ + text: string; + value: any; + }>; +}; diff --git a/app/mm-redux/types/jobs.ts b/app/mm-redux/types/jobs.ts new file mode 100644 index 000000000..cdde5fc57 --- /dev/null +++ b/app/mm-redux/types/jobs.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {IDMappedObjects} from './utilities'; +export type JobType = 'data_retention' | 'elasticsearch_post_indexing' | 'ldap_sync' | 'message_export'; +export type JobStatus = 'pending' | 'in_progress' | 'success' | 'error' | 'cancel_requested' | 'canceled'; +export type Job = { + id: string; + type: JobType; + priority: number; + create_at: number; + start_at: number; + last_activity_at: number; + status: JobStatus; + progress: number; + data: any; +}; +export type JobsByType = { + [x in JobType]?: Array; +}; +export type JobsState = { + jobs: IDMappedObjects; + jobsByTypeList: JobsByType; +}; diff --git a/app/mm-redux/types/module.d.ts b/app/mm-redux/types/module.d.ts new file mode 100644 index 000000000..3dd9c11aa --- /dev/null +++ b/app/mm-redux/types/module.d.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +declare module 'gfycat-sdk'; +declare module 'redux-offline'; +declare module 'redux-offline/lib/defaults'; +declare module 'remote-redux-devtools'; +declare module 'redux-persist'; +declare module 'redux-persist/constants'; +declare module 'redux-action-buffer'; \ No newline at end of file diff --git a/app/mm-redux/types/plugins.ts b/app/mm-redux/types/plugins.ts new file mode 100644 index 000000000..f484c199d --- /dev/null +++ b/app/mm-redux/types/plugins.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type ClientPluginManifest = { + id: string; + min_server_version?: string; + version: string; + webapp: { + bundle_path: string; + }; +} + +export type MarketplacePlugin = { + homepage_url: string; + download_url: string; + manifest: { + id: string; + name: string; + description: string; + version: string; + minServerVersion: string; + }; + installed_version: string; +} diff --git a/app/mm-redux/types/posts.ts b/app/mm-redux/types/posts.ts new file mode 100644 index 000000000..085ad7993 --- /dev/null +++ b/app/mm-redux/types/posts.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {CustomEmoji} from './emojis'; +import {FileInfo} from './files'; +import {Reaction} from './reactions'; +import {Channel} from './channels'; +import { + $ID, + RelationOneToOne, + RelationOneToMany, + IDMappedObjects, + Dictionary, +} from './utilities'; + +export type PostType = 'system_add_remove' | + 'system_add_to_channel' | + 'system_add_to_team' | + 'system_channel_deleted' | + 'system_channel_restored' | + 'system_displayname_change' | + 'system_convert_channel' | + 'system_ephemeral' | + 'system_header_change' | + 'system_join_channel' | + 'system_join_leave' | + 'system_leave_channel' | + 'system_purpose_change' | + 'system_remove_from_channel'; + +export type PostEmbedType = 'image' | 'message_attachment' | 'opengraph'; + +export type PostEmbed = { + type: PostEmbedType; + url: string; + data: Record; +}; + +export type PostImage = { + height: number; + width: number; +}; + +export type PostMetadata = { + embeds: Array; + emojis: Array; + files: Array; + images: Dictionary; + reactions: Array; +}; + +export type Post = { + id: string; + create_at: number; + update_at: number; + edit_at: number; + delete_at: number; + is_pinned: boolean; + user_id: string; + channel_id: string; + root_id: string; + parent_id: string; + original_id: string; + message: string; + type: PostType; + props: Record; + hashtags: string; + pending_post_id: string; + reply_count: number; + file_ids?: any[]; + metadata: PostMetadata; + failed?: boolean; + user_activity_posts?: Array; + state?: 'DELETED'; +}; + +export type PostWithFormatData = Post & { + isFirstReply: boolean; + isLastReply: boolean; + previousPostIsComment: boolean; + commentedOnPost?: Post; + consecutivePostByUser: boolean; + replyCount: number; + isCommentMention: boolean; + highlight: boolean; +}; + +export type PostOrderBlock = { + order: Array; + recent?: boolean; + oldest?: boolean; +}; + +export type MessageHistory = { + messages: Array; + index: { + post: number; + comment: number; + }; +}; + +export type PostsState = { + posts: IDMappedObjects; + postsInChannel: Dictionary>; + postsInThread: RelationOneToMany; + reactions: RelationOneToOne>; + openGraph: RelationOneToOne; + pendingPostIds: Array; + selectedPostId: string; + currentFocusedPostId: string; + messagesHistory: MessageHistory; + expandedURLs: Dictionary; +}; diff --git a/app/mm-redux/types/preferences.ts b/app/mm-redux/types/preferences.ts new file mode 100644 index 000000000..2965f5151 --- /dev/null +++ b/app/mm-redux/types/preferences.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type PreferenceType = { + category: string; + name: string; + user_id: string; + value?: string; +}; + +export type PreferencesType = { + [x: string]: PreferenceType; +}; + +export type Theme = { + [key: string]: string | undefined; + type?: string; + sidebarBg: string; + sidebarText: string; + sidebarUnreadText: string; + sidebarTextHoverBg: string; + sidebarTextActiveBorder: string; + sidebarTextActiveColor: string; + sidebarHeaderBg: string; + sidebarHeaderTextColor: string; + onlineIndicator: string; + awayIndicator: string; + dndIndicator: string; + mentionBg: string; + mentionBj: string; + mentionColor: string; + centerChannelBg: string; + centerChannelColor: string; + newMessageSeparator: string; + linkColor: string; + buttonBg: string; + buttonColor: string; + errorTextColor: string; + mentionHighlightBg: string; + mentionHighlightLink: string; + codeTheme: string; +}; \ No newline at end of file diff --git a/app/mm-redux/types/reactions.ts b/app/mm-redux/types/reactions.ts new file mode 100644 index 000000000..ee726c5e3 --- /dev/null +++ b/app/mm-redux/types/reactions.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type Reaction = { + user_id: string; + post_id: string; + emoji_name: string; + create_at: number; +}; diff --git a/app/mm-redux/types/requests.ts b/app/mm-redux/types/requests.ts new file mode 100644 index 000000000..5e25e17e5 --- /dev/null +++ b/app/mm-redux/types/requests.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type RequestStatusOption = 'not_started' | 'started' | 'success' | 'failure' | 'cancelled'; +export type RequestStatusType = { + status: RequestStatusOption; + error: null | Record; +}; + +export type ChannelsRequestsStatuses = { + getChannels: RequestStatusType; + getAllChannels: RequestStatusType; + myChannels: RequestStatusType; + createChannel: RequestStatusType; + updateChannel: RequestStatusType; +}; + +export type GeneralRequestsStatuses = { + websocket: RequestStatusType; +}; + +export type PostsRequestsStatuses = { + createPost: RequestStatusType; + editPost: RequestStatusType; + getPostThread: RequestStatusType; +}; + +export type TeamsRequestsStatuses = { + getMyTeams: RequestStatusType; + getTeams: RequestStatusType; + joinTeam: RequestStatusType; +}; + +export type UsersRequestsStatuses = { + checkMfa: RequestStatusType; + login: RequestStatusType; + logout: RequestStatusType; + autocompleteUsers: RequestStatusType; + updateMe: RequestStatusType; +}; + +export type EmojisRequestsStatuses = { + createCustomEmoji: RequestStatusType; + getCustomEmojis: RequestStatusType; + deleteCustomEmoji: RequestStatusType; + getAllCustomEmojis: RequestStatusType; + getCustomEmoji: RequestStatusType; +}; + +export type FilesRequestsStatuses = { + uploadFiles: RequestStatusType; +}; + +export type RolesRequestsStatuses = { + getRolesByNames: RequestStatusType; + getRoleByName: RequestStatusType; + getRole: RequestStatusType; + editRole: RequestStatusType; +}; + +export type JobsRequestsStatuses = { + createJob: RequestStatusType; + getJob: RequestStatusType; + getJobs: RequestStatusType; + cancelJob: RequestStatusType; +}; + +export type SearchRequestsStatuses = { + flaggedPosts: RequestStatusType; + pinnedPosts: RequestStatusType; +}; \ No newline at end of file diff --git a/app/mm-redux/types/roles.ts b/app/mm-redux/types/roles.ts new file mode 100644 index 000000000..4f612cb71 --- /dev/null +++ b/app/mm-redux/types/roles.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type ChannelModerationRoles = 'members' | 'guests'; + +export type Role = { + id: string; + name: string; + display_name: string; + description: string; + create_at: number; + update_at: number; + delete_at: number; + permissions: Array; + scheme_managed: boolean; + built_in: boolean; +}; diff --git a/app/mm-redux/types/schemes.ts b/app/mm-redux/types/schemes.ts new file mode 100644 index 000000000..9f2d5e318 --- /dev/null +++ b/app/mm-redux/types/schemes.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type SchemeScope = 'team' | 'channel'; +export type Scheme = { + id: string; + name: string; + description: string; + display_name: string; + create_at: number; + update_at: number; + delete_at: number; + scope: SchemeScope; + default_team_admin_role: string; + default_team_user_role: string; + default_channel_admin_role: string; + default_channel_user_role: string; +}; +export type SchemesState = { + schemes: { + [x: string]: Scheme; + }; +}; +export type SchemePatch = { + name?: string; + description?: string; +}; diff --git a/app/mm-redux/types/search.ts b/app/mm-redux/types/search.ts new file mode 100644 index 000000000..c8b8e10c9 --- /dev/null +++ b/app/mm-redux/types/search.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Dictionary} from '@mm-redux/types/utilities'; + +export type Search = { + terms: string; + isOrSearch: boolean; +}; + +export type SearchState = { + current: any; + results: Array; + flagged: Array; + pinned: Dictionary>; + isSearchingTerm: boolean; + isSearchGettingMore: boolean; + recent: { + [x: string]: Array; + }; + matches: { + [x: string]: Array; + }; +}; + +export type SearchParameter = { + terms: string; + is_or_search: boolean; + time_zone_offset?: number; + page: number; + per_page: number; + include_deleted_channels: boolean; +} \ No newline at end of file diff --git a/app/mm-redux/types/store.ts b/app/mm-redux/types/store.ts new file mode 100644 index 000000000..542970cb4 --- /dev/null +++ b/app/mm-redux/types/store.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GeneralState} from './general'; +import {UsersState} from './users'; +import {TeamsState} from './teams'; +import {ChannelsState} from './channels'; +import {PostsState} from './posts'; +import {JobsState} from './jobs'; +import {SearchState} from './search'; +import {IntegrationsState} from './integrations'; +import {FilesState} from './files'; +import {EmojisState} from './emojis'; +import {SchemesState} from './schemes'; +import {Typing} from './typing'; +import {GroupsState} from './groups'; +import {ChannelsRequestsStatuses, GeneralRequestsStatuses, PostsRequestsStatuses, TeamsRequestsStatuses, UsersRequestsStatuses, FilesRequestsStatuses, RolesRequestsStatuses, JobsRequestsStatuses} from './requests'; +import {Role} from './roles'; +import {PreferenceType} from './preferences'; +import {Bot} from './bots'; +import {ChannelCategoriesState} from './channel_categories'; +import {Dictionary} from './utilities'; + +export type GlobalState = { + entities: { + general: GeneralState; + users: UsersState; + teams: TeamsState; + channels: ChannelsState; + posts: PostsState; + bots: { + accounts: Dictionary; + }; + preferences: { + myPreferences: { + [x: string]: PreferenceType; + }; + }; + jobs: JobsState; + search: SearchState; + integrations: IntegrationsState; + files: FilesState; + emojis: EmojisState; + typing: Typing; + roles: { + roles: { + [x: string]: Role; + }; + pending: Set; + }; + schemes: SchemesState; + gifs: any; + groups: GroupsState; + channelCategories: ChannelCategoriesState; + }; + errors: Array; + requests: { + channels: ChannelsRequestsStatuses; + general: GeneralRequestsStatuses; + posts: PostsRequestsStatuses; + teams: TeamsRequestsStatuses; + users: UsersRequestsStatuses; + files: FilesRequestsStatuses; + roles: RolesRequestsStatuses; + jobs: JobsRequestsStatuses; + }; + websocket: { + connected: boolean; + lastConnectAt: number; + lastDisconnectAt: number; + }; +}; diff --git a/app/mm-redux/types/teams.ts b/app/mm-redux/types/teams.ts new file mode 100644 index 000000000..ac51b7d8d --- /dev/null +++ b/app/mm-redux/types/teams.ts @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Dictionary} from './utilities'; +import {Error} from './errors'; + +export type TeamMembership = { + mention_count: number; + msg_count: number; + team_id: string; + user_id: string; + roles: string; + delete_at: number; + scheme_user: boolean; + scheme_admin: boolean; +}; + +export type TeamMemberWithError = { + member: TeamMembership; + user_id: string; + error: Error; +} + +export type TeamType = 'O' | 'I'; + +export type Team = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + display_name: string; + name: string; + description: string; + email: string; + type: TeamType; + company_name: string; + allowed_domains: string; + invite_id: string; + allow_open_invite: boolean; + scheme_id: string; + group_constrained: boolean; +}; + +export type TeamsState = { + currentTeamId: string; + teams: Dictionary; + myMembers: Dictionary; + membersInTeam: any; + stats: any; + groupsAssociatedToTeam: any; + totalCount: number; +}; + +export type TeamUnread = { + team_id: string; + mention_count: number; + msg_count: number; +}; \ No newline at end of file diff --git a/app/mm-redux/types/typing.ts b/app/mm-redux/types/typing.ts new file mode 100644 index 000000000..36f867fec --- /dev/null +++ b/app/mm-redux/types/typing.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type Typing = { + [x: string]: { + [x: string]: number; + }; +}; diff --git a/app/mm-redux/types/users.ts b/app/mm-redux/types/users.ts new file mode 100644 index 000000000..367a08e58 --- /dev/null +++ b/app/mm-redux/types/users.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Channel} from './channels'; +import {Team} from './teams'; +import {PostType} from './posts'; +import {$ID, IDMappedObjects, RelationOneToMany, RelationOneToOne} from './utilities'; +export type UserNotifyProps = { + desktop: 'default' | 'all' | 'mention' | 'none'; + desktop_sound: 'true' | 'false'; + email: 'true' | 'false'; + mark_unread: 'all' | 'mention'; + push: 'default' | 'all' | 'mention' | 'none'; + push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online'; + comments: 'never' | 'root' | 'any'; + first_name: 'true' | 'false'; + channel: 'true' | 'false'; + mention_keys: string; +}; +export type UserProfile = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + username: string; + auth_data: string; + auth_service: string; + email: string; + email_verified: boolean; + nickname: string; + first_name: string; + last_name: string; + position: string; + roles: string; + locale: string; + notify_props: UserNotifyProps; + terms_of_service_id: string; + terms_of_service_create_at: number; + timezone?: UserTimezone; + is_bot: boolean; + last_picture_update: number; +}; +export type UsersState = { + currentUserId: string; + isManualStatus: RelationOneToOne; + mySessions: Array; + myAudits: Array; + profiles: IDMappedObjects; + profilesInTeam: RelationOneToMany; + profilesNotInTeam: RelationOneToMany; + profilesWithoutTeam: Set; + profilesInChannel: RelationOneToMany; + profilesNotInChannel: RelationOneToMany; + statuses: RelationOneToOne; + stats: any; +}; +export type UserTimezone = { + useAutomaticTimezone: boolean | string; + automaticTimezone: string; + manualTimezone: string; +}; +export type UserActivity = { + [x in PostType]: { + [x in $ID]: | { + ids: Array<$ID>; + usernames: Array; + } | Array<$ID>; + }; +}; + +export type UserStatus = { + user_id: string; + status: string; + manual: boolean; + last_activity_at: number; + active_channel?: string; +} diff --git a/app/mm-redux/types/utilities.ts b/app/mm-redux/types/utilities.ts new file mode 100644 index 000000000..99cc1abd4 --- /dev/null +++ b/app/mm-redux/types/utilities.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export type $ID = E['id']; +export type $UserID = E['user_id']; +export type $Name = E['name']; +export type $Username = E['username']; +export type $Email = E['email']; +export type RelationOneToOne = { + [x in $ID]: T; +}; +export type RelationOneToMany = { + [x in $ID]: Array<$ID>; +}; +export type IDMappedObjects = RelationOneToOne; +export type UserIDMappedObjects = { + [x in $UserID]: E; +}; +export type NameMappedObjects = { + [x in $Name]: E; +}; +export type UsernameMappedObjects = { + [x in $Username]: E; +}; +export type EmailMappedObjects = { + [x in $Email]: E; +}; + +export type Dictionary = { + [key: string]: T; +}; \ No newline at end of file diff --git a/app/mm-redux/utils/channel_utils.test.js b/app/mm-redux/utils/channel_utils.test.js new file mode 100644 index 000000000..809b0da2a --- /dev/null +++ b/app/mm-redux/utils/channel_utils.test.js @@ -0,0 +1,294 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {General, Users} from '../constants'; +import TestHelper from 'test/test_helper'; + +import { + areChannelMentionsIgnored, + canManageMembersOldPermissions, + compareNotifyProps, + isAutoClosed, + isChannelMuted, + filterChannelsMatchingTerm, + sortChannelsByRecency, + sortChannelsByDisplayName, +} from '@mm-redux/utils/channel_utils'; + +describe('ChannelUtils', () => { + it('canManageMembersOldPermissions', () => { + const notLicensed = {IsLicensed: 'false'}; + const licensed = {IsLicensed: 'true'}; + + const anyoneCanManageMembers = {RestrictPrivateChannelManageMembers: General.PERMISSIONS_ANY}; + const channelAdminsCanManageMembers = {RestrictPrivateChannelManageMembers: General.PERMISSIONS_CHANNEL_ADMIN}; + const teamAdminsCanManageMembers = {RestrictPrivateChannelManageMembers: General.PERMISSIONS_TEAM_ADMIN}; + const systemAdminsCanManageMembers = {RestrictPrivateChannelManageMembers: General.PERMISSIONS_SYSTEM_ADMIN}; + + const townSquareChannel = {name: General.DEFAULT_CHANNEL, type: General.OPEN_CHANNEL}; + const publicChannel = {type: General.PUBLIC_CHANNEL}; + const privateChannel = {type: General.PRIVATE_CHANNEL}; + const gmChannel = {type: General.GM_CHANNEL}; + const dmChannel = {type: General.DM_CHANNEL}; + + const systemAdmin = {roles: General.SYSTEM_USER_ROLE + ' ' + General.SYSTEM_ADMIN_ROLE}; + const systemUser = {roles: General.SYSTEM_USER_ROLE}; + + const teamAdmin = {roles: General.TEAM_USER_ROLE + ' ' + General.TEAM_ADMIN_ROLE}; + const teamUser = {roles: General.TEAM_USER_ROLE}; + + const channelAdmin = {roles: General.CHANNEL_USER_ROLE + ' ' + General.CHANNEL_ADMIN_ROLE}; + const channelUser = {roles: General.CHANNEL_USER_ROLE}; + + // No one can manage users of town square + assert.ok(!canManageMembersOldPermissions(townSquareChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, notLicensed)); + assert.ok(!canManageMembersOldPermissions(townSquareChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, licensed)); + + // Or DM/GM channels + assert.ok(!canManageMembersOldPermissions(dmChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, notLicensed)); + assert.ok(!canManageMembersOldPermissions(dmChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(gmChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, notLicensed)); + assert.ok(!canManageMembersOldPermissions(gmChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, licensed)); + + // Everyone can manage users of public channels + assert.ok(canManageMembersOldPermissions(publicChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemUser, teamUser, channelUser, anyoneCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemAdmin, teamAdmin, channelAdmin, systemAdminsCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemUser, teamUser, channelUser, systemAdminsCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemUser, teamUser, channelUser, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemAdmin, teamAdmin, channelAdmin, systemAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(publicChannel, systemUser, teamUser, channelUser, systemAdminsCanManageMembers, licensed)); + + // And private channels if not licensed + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelUser, anyoneCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelAdmin, systemAdminsCanManageMembers, notLicensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelUser, systemAdminsCanManageMembers, notLicensed)); + + // But it gets complicated when you have a license + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelAdmin, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelAdmin, channelAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelAdmin, teamAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelAdmin, systemAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelUser, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelUser, channelAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelUser, teamAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamAdmin, channelUser, systemAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelAdmin, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelAdmin, channelAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelAdmin, teamAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelAdmin, systemAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelUser, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelUser, channelAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelUser, teamAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemAdmin, teamUser, channelUser, systemAdminsCanManageMembers, licensed)); + + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelAdmin, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelAdmin, channelAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelAdmin, teamAdminsCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelAdmin, systemAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelUser, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelUser, channelAdminsCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelUser, teamAdminsCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamAdmin, channelUser, systemAdminsCanManageMembers, licensed)); + + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelAdmin, anyoneCanManageMembers, licensed)); + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelAdmin, channelAdminsCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelAdmin, teamAdminsCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelAdmin, systemAdminsCanManageMembers, licensed)); + + assert.ok(canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelUser, anyoneCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelUser, channelAdminsCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelUser, teamAdminsCanManageMembers, licensed)); + assert.ok(!canManageMembersOldPermissions(privateChannel, systemUser, teamUser, channelUser, systemAdminsCanManageMembers, licensed)); + }); + + it('isAutoClosed', () => { + const autoCloseEnabled = {CloseUnusedDirectMessages: 'true'}; + const autoCloseDisabled = {CloseUnusedDirectMessages: 'false'}; + const activeChannel = {id: 'channelid', last_post_at: new Date().getTime()}; + const inactiveChannel = {id: 'channelid', last_post_at: 1}; + const now = new Date().getTime(); + + assert.ok(isAutoClosed(autoCloseEnabled, {}, inactiveChannel)); + + assert.ok(isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + }, inactiveChannel)); + + assert.ok(!isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + }, inactiveChannel, now)); + + assert.ok(!isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + }, activeChannel)); + + assert.ok(!isAutoClosed(autoCloseDisabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + }, inactiveChannel)); + + assert.ok(!isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + 'channel_open_time--channelid': {value: now.toString()}, + }, inactiveChannel)); + + assert.ok(!isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'never'}, + }, inactiveChannel)); + + assert.ok(isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + 'channel_open_time--channelid': {value: (now - 1000).toString()}, + }, inactiveChannel, 0, now)); + + assert.ok(!isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + 'channel_open_time--channelid': {value: now.toString()}, + }, inactiveChannel, 0, now - 1000)); + + assert.ok(!isAutoClosed(autoCloseEnabled, { + 'sidebar_settings--close_unused_direct_messages': {value: 'after_seven_days'}, + 'channel_open_time--channelid': {value: (now - 1000).toString()}, + }, inactiveChannel, 0, now, 'channelid')); + }); + + it('areChannelMentionsIgnored', () => { + const currentUserNotifyProps1 = {channel: 'true'}; + const channelMemberNotifyProps1 = {ignore_channel_mentions: Users.IGNORE_CHANNEL_MENTIONS_DEFAULT}; + assert.equal(false, areChannelMentionsIgnored(channelMemberNotifyProps1, currentUserNotifyProps1)); + + const currentUserNotifyProps2 = {channel: 'true'}; + const channelMemberNotifyProps2 = {ignore_channel_mentions: Users.IGNORE_CHANNEL_MENTIONS_OFF}; + assert.equal(false, areChannelMentionsIgnored(channelMemberNotifyProps2, currentUserNotifyProps2)); + + const currentUserNotifyProps3 = {channel: 'true'}; + const channelMemberNotifyProps3 = {ignore_channel_mentions: Users.IGNORE_CHANNEL_MENTIONS_ON}; + assert.equal(true, areChannelMentionsIgnored(channelMemberNotifyProps3, currentUserNotifyProps3)); + + const currentUserNotifyProps4 = {channel: 'false'}; + const channelMemberNotifyProps4 = {ignore_channel_mentions: Users.IGNORE_CHANNEL_MENTIONS_DEFAULT}; + assert.equal(true, areChannelMentionsIgnored(channelMemberNotifyProps4, currentUserNotifyProps4)); + + const currentUserNotifyProps5 = {channel: 'false'}; + const channelMemberNotifyProps5 = {ignore_channel_mentions: Users.IGNORE_CHANNEL_MENTIONS_OFF}; + assert.equal(false, areChannelMentionsIgnored(channelMemberNotifyProps5, currentUserNotifyProps5)); + + const currentUserNotifyProps6 = {channel: 'false'}; + const channelMemberNotifyProps6 = {ignore_channel_mentions: Users.IGNORE_CHANNEL_MENTIONS_ON}; + assert.equal(true, areChannelMentionsIgnored(channelMemberNotifyProps6, currentUserNotifyProps6)); + + const currentUserNotifyProps7 = {channel: true}; + const channelMemberNotifyProps7 = null; + assert.equal(false, areChannelMentionsIgnored(channelMemberNotifyProps7, currentUserNotifyProps7)); + + const currentUserNotifyProps8 = {channel: false}; + const channelMemberNotifyProps8 = null; + assert.equal(false, areChannelMentionsIgnored(channelMemberNotifyProps8, currentUserNotifyProps8)); + }); + + it('filterChannelsMatchingTerm', () => { + const channel1 = TestHelper.fakeChannel(); + channel1.display_name = 'channel1'; + channel1.name = 'blargh1'; + const channel2 = TestHelper.fakeChannel(); + channel2.display_name = 'channel2'; + channel2.name = 'blargh2'; + const channels = [channel1, channel2]; + + assert.deepEqual(filterChannelsMatchingTerm(channels, 'chan'), channels); + assert.deepEqual(filterChannelsMatchingTerm(channels, 'CHAN'), channels); + assert.deepEqual(filterChannelsMatchingTerm(channels, 'blargh'), channels); + assert.deepEqual(filterChannelsMatchingTerm(channels, 'channel1'), [channel1]); + assert.deepEqual(filterChannelsMatchingTerm(channels, 'junk'), []); + assert.deepEqual(filterChannelsMatchingTerm(channels, 'annel'), []); + }); + + it('sortChannelsByRecency', () => { + const channelA = TestHelper.fakeChannel(); + channelA.id = 'channel_a'; + channelA.last_post_at = 1; + + const channelB = TestHelper.fakeChannel(); + channelB.last_post_at = 2; + channelB.id = 'channel_b'; + + // sorting depends on channel's last_post_at when both channels don't have last post + assert.deepEqual(sortChannelsByRecency({}, channelA, channelB), 1); + + // sorting depends on create_at of channel's last post if it's greater than the channel's last_post_at + const lastPosts = { + channel_a: {id: 'post_id_1', create_at: 5, update_at: 5}, + channel_b: {id: 'post_id_2', create_at: 7, update_at: 7}, + }; + assert.deepEqual(sortChannelsByRecency(lastPosts, channelA, channelB), 2, 'should return 2, comparison of create_at (7 - 5)'); + + // sorting remains the same even if channel's last post is updated (e.g. edited, updated reaction, etc) + lastPosts.channel_a.update_at = 10; + assert.deepEqual(sortChannelsByRecency(lastPosts, channelA, channelB), 2, 'should return 2, comparison of create_at (7 - 5)'); + + // sorting depends on create_at of channel's last post if it's greater than the channel's last_post_at + lastPosts.channel_a.create_at = 10; + assert.deepEqual(sortChannelsByRecency(lastPosts, channelA, channelB), -3, 'should return 2, comparison of create_at (7 - 10)'); + }); + + it('sortChannelsByDisplayName', () => { + const channelA = { + name: 'channelA', + team_id: 'teamId', + display_name: 'Unit Test channelA', + type: 'O', + delete_at: 0, + total_msg_count: 0, + }; + + const channelB = { + name: 'channelB', + team_id: 'teamId', + display_name: 'Unit Test channelB', + type: 'O', + delete_at: 0, + total_msg_count: 0, + }; + + assert.equal(sortChannelsByDisplayName('en', channelA, channelB), -1); + assert.equal(sortChannelsByDisplayName('en', channelB, channelA), 1); + + // When a channel does not have a display name set + Reflect.deleteProperty(channelB, 'display_name'); + assert.equal(sortChannelsByDisplayName('en', channelA, channelB), -1); + assert.equal(sortChannelsByDisplayName('en', channelB, channelA), 1); + }); + + it('isChannelMuted', () => { + const mutedChannelMember = { + notify_props: {mark_unread: 'mention'}, + }; + + const unmutedChannelMember = { + notify_props: {mark_unread: 'all'}, + }; + + assert.equal(true, isChannelMuted(mutedChannelMember)); + assert.equal(false, isChannelMuted(unmutedChannelMember)); + }); + + it('compareNotifyProps', () => { + const baseProps = { + desktop: 'default', + email: 'all', + mark_unread: 'mention', + push: 'default', + ignore_channel_mentions: 'on', + }; + + assert.equal(true, compareNotifyProps(baseProps, {...baseProps})); + assert.equal(false, compareNotifyProps(baseProps, {...baseProps, desktop: 'all'})); + assert.equal(false, compareNotifyProps(baseProps, {...baseProps, email: 'mention'})); + assert.equal(false, compareNotifyProps(baseProps, {...baseProps, push: 'none'})); + assert.equal(false, compareNotifyProps(baseProps, {...baseProps, ignore_channel_mentions: 'off'})); + }); +}); diff --git a/app/mm-redux/utils/channel_utils.ts b/app/mm-redux/utils/channel_utils.ts new file mode 100644 index 000000000..e8d54d5ee --- /dev/null +++ b/app/mm-redux/utils/channel_utils.ts @@ -0,0 +1,734 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {General, Preferences, Permissions, Users} from '../constants'; + +import {hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import {haveITeamPermission, haveIChannelPermission} from '@mm-redux/selectors/entities/roles'; +import {Channel, ChannelMembership, ChannelType, ChannelNotifyProps} from '@mm-redux/types/channels'; +import {Post} from '@mm-redux/types/posts'; +import {UserProfile, UsersState, UserNotifyProps} from '@mm-redux/types/users'; +import {GlobalState} from '@mm-redux/types/store'; +import {TeamMembership} from '@mm-redux/types/teams'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {RelationOneToOne, IDMappedObjects} from '@mm-redux/types/utilities'; + +import {getPreferenceKey, getPreferencesByCategory} from './preference_utils'; +import {displayUsername} from './user_utils'; + +const channelTypeOrder = { + [General.OPEN_CHANNEL]: 0, + [General.PRIVATE_CHANNEL]: 1, + [General.DM_CHANNEL]: 2, + [General.GM_CHANNEL]: 2, +}; + +/** + * Returns list of sorted channels grouped by type. Favorites here is considered as separated type. + * + * Example: { + * publicChannels: [...], + * privateChannels: [...], + * directAndGroupChannels: [...], + * favoriteChannels: [...] + * } + */ +export function buildDisplayableChannelListWithUnreadSection(usersState: UsersState, myChannels: Array, myMembers: RelationOneToOne, config: any, myPreferences: { + [x: string]: PreferenceType; +}, teammateNameDisplay: string, lastPosts: RelationOneToOne) { + const { + currentUserId, + profiles, + } = usersState; + const locale = getUserLocale(currentUserId, profiles); + const missingDirectChannels = createMissingDirectChannels(currentUserId, myChannels, myPreferences); + const channels = buildChannels(usersState, myChannels, missingDirectChannels, teammateNameDisplay, locale); + const unreadChannels = [...buildChannelsWithMentions(channels, myMembers, locale), ...buildUnreadChannels(channels, myMembers, locale)]; + const notUnreadChannels = channels.filter((channel: Channel) => !isUnreadChannel(myMembers, channel)); + const favoriteChannels = buildFavoriteChannels(notUnreadChannels, myPreferences, locale); + const notFavoriteChannels = buildNotFavoriteChannels(notUnreadChannels, myPreferences); + const directAndGroupChannels = buildDirectAndGroupChannels(notFavoriteChannels, myMembers, config, myPreferences, currentUserId, profiles, lastPosts); + return { + unreadChannels, + favoriteChannels, + publicChannels: (notFavoriteChannels.filter(isOpenChannel) as Array), + privateChannels: (notFavoriteChannels.filter(isPrivateChannel) as Array), + directAndGroupChannels, + }; +} + +export function completeDirectChannelInfo(usersState: UsersState, teammateNameDisplay: string, channel: Channel): Channel { + if (isDirectChannel(channel)) { + const teammateId = getUserIdFromChannelName(usersState.currentUserId, channel.name); + + // return empty string instead of `someone` default string for display_name + return { + ...channel, + display_name: displayUsername(usersState.profiles[teammateId], teammateNameDisplay, false), + teammate_id: teammateId, + status: usersState.statuses[teammateId] || 'offline', + }; + } else if (isGroupChannel(channel)) { + return completeDirectGroupInfo(usersState, teammateNameDisplay, channel); + } + + return channel; +} + +export function completeDirectChannelDisplayName(currentUserId: string, profiles: IDMappedObjects, userIdsInChannel: Set, teammateNameDisplay: string, channel: Channel): Channel { + if (isDirectChannel(channel)) { + const dmChannelClone = {...channel}; + const teammateId = getUserIdFromChannelName(currentUserId, channel.name); + + return Object.assign(dmChannelClone, {display_name: displayUsername(profiles[teammateId], teammateNameDisplay)}); + } else if (isGroupChannel(channel) && userIdsInChannel && userIdsInChannel.size > 0) { + const displayName = getGroupDisplayNameFromUserIds(Array.from(userIdsInChannel), profiles, currentUserId, teammateNameDisplay); + return {...channel, display_name: displayName}; + } + + return channel; +} + +export function cleanUpUrlable(input: string): string { + let cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-'); + cleaned = cleaned.replace(/-{2,}/, '-'); + cleaned = cleaned.replace(/^-+/, ''); + cleaned = cleaned.replace(/-+$/, ''); + return cleaned; +} + +export function getChannelByName(channels: IDMappedObjects, name: string): Channel | undefined | null { + const channelIds = Object.keys(channels); + for (let i = 0; i < channelIds.length; i++) { + const id = channelIds[i]; + if (channels[id].name === name) { + return channels[id]; + } + } + return null; +} + +export function getDirectChannelName(id: string, otherId: string): string { + let handle; + + if (otherId > id) { + handle = id + '__' + otherId; + } else { + handle = otherId + '__' + id; + } + + return handle; +} + +export function getUserIdFromChannelName(userId: string, channelName: string): string { + const ids = channelName.split('__'); + let otherUserId = ''; + if (ids[0] === userId) { + otherUserId = ids[1]; + } else { + otherUserId = ids[0]; + } + + return otherUserId; +} + +export function isAutoClosed( + config: any, + myPreferences: { + [x: string]: PreferenceType; + }, + channel: Channel, + channelActivity: number, + channelArchiveTime: number, + currentChannelId = '', + now = Date.now() +): boolean { + const cutoff = now - 7 * 24 * 60 * 60 * 1000; + const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channel.id}`]; + const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0; + + // Note that viewTime is not set correctly at the time of writing + if (viewTime > cutoff) { + return false; + } + + const openTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_OPEN_TIME}--${channel.id}`]; + const openTime = openTimePref ? parseInt(openTimePref.value!, 10) : 0; + + // Only close archived channels when not being viewed + if (channel.id !== currentChannelId && channelArchiveTime && channelArchiveTime > openTime) { + return true; + } + + if (config.CloseUnusedDirectMessages !== 'true' || isFavoriteChannel(myPreferences, channel.id)) { + return false; + } + + const autoClose = myPreferences[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]; + if (!autoClose || autoClose.value === Preferences.AUTOCLOSE_DMS_ENABLED) { + if (channelActivity && channelActivity > cutoff) { + return false; + } + if (openTime > cutoff) { + return false; + } + const lastActivity = channel.last_post_at; + return !lastActivity || lastActivity < cutoff; + } + + return false; +} + +export function isDirectChannel(channel: Channel): boolean { + return channel.type === General.DM_CHANNEL; +} + +export function isDirectChannelVisible( + otherUserOrOtherUserId: UserProfile | string, + config: any, + myPreferences: { + [x: string]: PreferenceType; + }, + channel: Channel, + lastPost?: Post | null, + isUnread?: boolean, + currentChannelId = '', + now?: number +): boolean { + const otherUser = typeof otherUserOrOtherUserId === 'object' ? otherUserOrOtherUserId : null; + const otherUserId = typeof otherUserOrOtherUserId === 'object' ? otherUserOrOtherUserId.id : otherUserOrOtherUserId; + const dm = myPreferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${otherUserId}`]; + + if (!dm || dm.value !== 'true') { + return false; + } + + return isUnread || !isAutoClosed( + config, + myPreferences, + channel, + lastPost ? lastPost.create_at : 0, + otherUser ? otherUser.delete_at : 0, + currentChannelId, + now + ); +} + +export function isGroupChannel(channel: Channel): boolean { + return channel.type === General.GM_CHANNEL; +} + +export function isGroupChannelVisible( + config: any, + myPreferences: { + [x: string]: PreferenceType; + }, + channel: Channel, + lastPost?: Post, + isUnread?: boolean, + now?: number +): boolean { + const gm = myPreferences[`${Preferences.CATEGORY_GROUP_CHANNEL_SHOW}--${channel.id}`]; + + if (!gm || gm.value !== 'true') { + return false; + } + + return isUnread || !isAutoClosed( + config, + myPreferences, + channel, + lastPost ? lastPost.create_at : 0, + 0, + '', + now + ); +} + +export function isGroupOrDirectChannelVisible( + channel: Channel, + memberships: RelationOneToOne, + config: any, + myPreferences: { + [x: string]: PreferenceType; + }, + currentUserId: string, + users: IDMappedObjects, + lastPosts: RelationOneToOne, + currentChannelId?: string, + now?: number +): boolean { + const lastPost = lastPosts[channel.id]; + + if (isGroupChannel(channel) && isGroupChannelVisible(config, myPreferences, channel, lastPost, isUnreadChannel(memberships, channel), now)) { + return true; + } + + if (!isDirectChannel(channel)) { + return false; + } + + const otherUserId = getUserIdFromChannelName(currentUserId, channel.name); + + return isDirectChannelVisible( + users[otherUserId] || otherUserId, + config, + myPreferences, + channel, + lastPost, + isUnreadChannel(memberships, channel), + currentChannelId, + now + ); +} + +export function showCreateOption(state: GlobalState, config: any, license: any, teamId: string, channelType: ChannelType, isAdmin: boolean, isSystemAdmin: boolean): boolean { + if (hasNewPermissions(state)) { + if (channelType === General.OPEN_CHANNEL) { + return haveITeamPermission(state, {team: teamId, permission: Permissions.CREATE_PUBLIC_CHANNEL}); + } else if (channelType === General.PRIVATE_CHANNEL) { + return haveITeamPermission(state, {team: teamId, permission: Permissions.CREATE_PRIVATE_CHANNEL}); + } + return true; + } + + if (license.IsLicensed !== 'true') { + return true; + } + + // Backwards compatibility with pre-advanced permissions config settings. + if (channelType === General.OPEN_CHANNEL) { + if (config.RestrictPublicChannelCreation === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { + return false; + } else if (config.RestrictPublicChannelCreation === General.TEAM_ADMIN_ROLE && !isAdmin) { + return false; + } + } else if (channelType === General.PRIVATE_CHANNEL) { + if (config.RestrictPrivateChannelCreation === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { + return false; + } else if (config.RestrictPrivateChannelCreation === General.TEAM_ADMIN_ROLE && !isAdmin) { + return false; + } + } + + return true; +} + +export function showManagementOptions(state: GlobalState, config: any, license: any, channel: Channel, isAdmin: boolean, isSystemAdmin: boolean, isChannelAdmin: boolean): boolean { + if (hasNewPermissions(state)) { + if (channel.type === General.OPEN_CHANNEL) { + return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES}); + } else if (channel.type === General.PRIVATE_CHANNEL) { + return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES}); + } + return true; + } + + if (license.IsLicensed !== 'true') { + return true; + } + + // Backwards compatibility with pre-advanced permissions config settings. + if (channel.type === General.OPEN_CHANNEL) { + if (config.RestrictPublicChannelManagement === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { + return false; + } + if (config.RestrictPublicChannelManagement === General.TEAM_ADMIN_ROLE && !isAdmin) { + return false; + } + if (config.RestrictPublicChannelManagement === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { + return false; + } + } else if (channel.type === General.PRIVATE_CHANNEL) { + if (config.RestrictPrivateChannelManagement === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { + return false; + } + if (config.RestrictPrivateChannelManagement === General.TEAM_ADMIN_ROLE && !isAdmin) { + return false; + } + if (config.RestrictPrivateChannelManagement === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { + return false; + } + } + + return true; +} + +export function showDeleteOption(state: GlobalState, config: any, license: any, channel: Channel, isAdmin: boolean, isSystemAdmin: boolean, isChannelAdmin: boolean): boolean { + if (hasNewPermissions(state)) { + if (channel.type === General.OPEN_CHANNEL) { + return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.DELETE_PUBLIC_CHANNEL}); + } else if (channel.type === General.PRIVATE_CHANNEL) { + return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.DELETE_PRIVATE_CHANNEL}); + } + return true; + } + + if (license.IsLicensed !== 'true') { + return true; + } + + // Backwards compatibility with pre-advanced permissions config settings. + if (channel.type === General.OPEN_CHANNEL) { + if (config.RestrictPublicChannelDeletion === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { + return false; + } + if (config.RestrictPublicChannelDeletion === General.TEAM_ADMIN_ROLE && !isAdmin) { + return false; + } + if (config.RestrictPublicChannelDeletion === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { + return false; + } + } else if (channel.type === General.PRIVATE_CHANNEL) { + if (config.RestrictPrivateChannelDeletion === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { + return false; + } + if (config.RestrictPrivateChannelDeletion === General.TEAM_ADMIN_ROLE && !isAdmin) { + return false; + } + if (config.RestrictPrivateChannelDeletion === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { + return false; + } + } + + return true; +} + +// Backwards compatibility with pre-advanced permissions config settings. + +export function canManageMembersOldPermissions(channel: Channel, user: UserProfile, teamMember: TeamMembership, channelMember: ChannelMembership, config: any, license: any): boolean { + if (channel.type === General.DM_CHANNEL || + channel.type === General.GM_CHANNEL || + channel.name === General.DEFAULT_CHANNEL) { + return false; + } + + if (license.IsLicensed !== 'true') { + return true; + } + + if (channel.type === General.PRIVATE_CHANNEL) { + const isSystemAdmin = user.roles.includes(General.SYSTEM_ADMIN_ROLE); + if (config.RestrictPrivateChannelManageMembers === General.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + return false; + } + + const isTeamAdmin = teamMember.roles.includes(General.TEAM_ADMIN_ROLE); + if (config.RestrictPrivateChannelManageMembers === General.PERMISSIONS_TEAM_ADMIN && !isTeamAdmin && !isSystemAdmin) { + return false; + } + + const isChannelAdmin = channelMember.roles.includes(General.CHANNEL_ADMIN_ROLE); + if (config.RestrictPrivateChannelManageMembers === General.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isTeamAdmin && !isSystemAdmin) { + return false; + } + } + + return true; +} + +export function getChannelsIdForTeam(state: GlobalState, teamId: string): Array { + const {channels} = state.entities.channels; + + return Object.keys(channels).map((key) => channels[key]).reduce((res, channel: Channel) => { + if (channel.team_id === teamId) { + res.push(channel.id); + } + return res; + }, [] as string[]); +} + +export function getGroupDisplayNameFromUserIds(userIds: Array, profiles: IDMappedObjects, currentUserId: string, teammateNameDisplay: string): string { + const names: string[] = []; + userIds.forEach((id) => { + if (id !== currentUserId) { + names.push(displayUsername(profiles[id], teammateNameDisplay)); + } + }); + + function sortUsernames(a: string, b: string) { + const locale = getUserLocale(currentUserId, profiles); + return a.localeCompare(b, locale, {numeric: true}); + } + + return names.sort(sortUsernames).join(', '); +} + +export function isFavoriteChannel(myPreferences: { + [x: string]: PreferenceType; +}, id: string) { + const fav = myPreferences[`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${id}`]; + return fav ? fav.value === 'true' : false; +} + +export function isDefault(channel: Channel): boolean { + return channel.name === General.DEFAULT_CHANNEL; +} + +//==================================================== + +function createFakeChannel(userId: string, otherUserId: string): Channel { + return { + name: getDirectChannelName(userId, otherUserId), + create_at: 0, + update_at: 0, + delete_at: 0, + extra_update_at: 0, + last_post_at: 0, + total_msg_count: 0, + type: General.DM_CHANNEL as ChannelType, + fake: true, + team_id: '', + scheme_id: '', + purpose: '', + header: '', + id: '', + display_name: '', + creator_id: '', + group_constrained: false, + }; +} + +function createFakeChannelCurried(userId: string): (a: string) => Channel { + return (otherUserId) => createFakeChannel(userId, otherUserId); +} + +function createMissingDirectChannels(currentUserId: string, allChannels: Array, myPreferences: { + [x: string]: PreferenceType; +}): Array { + const directChannelsDisplayPreferences = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); + + return Array. + from(directChannelsDisplayPreferences). + filter((entry) => entry[1] === 'true'). + map((entry) => entry[0]). + filter((teammateId) => !allChannels.some(isDirectChannelForUser.bind(null, currentUserId, teammateId))). + map(createFakeChannelCurried(currentUserId)); +} + +function completeDirectGroupInfo(usersState: UsersState, teammateNameDisplay: string, channel: Channel) { + const {currentUserId, profiles, profilesInChannel} = usersState; + const profilesIds = profilesInChannel[channel.id]; + const gm = {...channel}; + + if (profilesIds) { + gm.display_name = getGroupDisplayNameFromUserIds(profilesIds, profiles, currentUserId, teammateNameDisplay); + return gm; + } + + const usernames = gm.display_name.split(', '); + const users = Object.keys(profiles).map((key) => profiles[key]); + const userIds: string[] = []; + usernames.forEach((username: string) => { + const u = users.find((p): boolean => p.username === username); + if (u) { + userIds.push(u.id); + } + }); + if (usernames.length === userIds.length) { + gm.display_name = getGroupDisplayNameFromUserIds(userIds, profiles, currentUserId, teammateNameDisplay); + return gm; + } + + return channel; +} + +function isDirectChannelForUser(userId: string, otherUserId: string, channel: Channel) { + return channel.type === General.DM_CHANNEL && getUserIdFromChannelName(userId, channel.name) === otherUserId; +} + +function channelHasMentions(members: RelationOneToOne, channel: Channel) { + const member = members[channel.id]; + if (member) { + return member.mention_count > 0; + } + return false; +} + +function channelHasUnreadMessages(members: RelationOneToOne, channel: Channel): boolean { + const member = members[channel.id]; + if (member) { + const msgCount = channel.total_msg_count - member.msg_count; + const onlyMentions = member.notify_props && member.notify_props.mark_unread === General.MENTION; + return (Boolean(msgCount) && !onlyMentions && member.mention_count === 0); + } + + return false; +} + +export function isUnreadChannel(members: RelationOneToOne, channel: Channel): boolean { + const member = members[channel.id]; + if (member) { + const msgCount = channel.total_msg_count - member.msg_count; + const onlyMentions = member.notify_props && member.notify_props.mark_unread === General.MENTION; + return (member.mention_count > 0 || (Boolean(msgCount) && !onlyMentions)); + } + + return false; +} + +function isNotDeletedChannel(channel: Channel) { + return channel.delete_at === 0; +} + +export function isOpenChannel(channel: Channel): boolean { + return channel.type === General.OPEN_CHANNEL; +} + +export function isPrivateChannel(channel: Channel): boolean { + return channel.type === General.PRIVATE_CHANNEL; +} + +export function sortChannelsByTypeAndDisplayName(locale: string, a: Channel, b: Channel): number { + if (channelTypeOrder[a.type] !== channelTypeOrder[b.type]) { + if (channelTypeOrder[a.type] < channelTypeOrder[b.type]) { + return -1; + } + + return 1; + } + + const aDisplayName = filterName(a.display_name); + const bDisplayName = filterName(b.display_name); + + if (aDisplayName !== bDisplayName) { + return aDisplayName.toLowerCase().localeCompare(bDisplayName.toLowerCase(), locale, {numeric: true}); + } + + return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true}); +} + +function filterName(name: string): string { + return name.replace(/[.,'"\/#!$%\^&\*;:{}=\-_`~()]/g, ''); // eslint-disable-line no-useless-escape +} + +export function sortChannelsByDisplayName(locale: string, a: Channel, b: Channel): number { + // if both channels have the display_name defined + if (a.display_name && b.display_name && a.display_name !== b.display_name) { + return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true}); + } + + return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true}); +} + +export function sortChannelsByDisplayNameAndMuted(locale: string, members: RelationOneToOne, a: Channel, b: Channel): number { + const aMember = members[a.id]; + const bMember = members[b.id]; + + if (isChannelMuted(bMember) === isChannelMuted(aMember)) { + return sortChannelsByDisplayName(locale, a, b); + } + + if (!isChannelMuted(bMember) && isChannelMuted(aMember)) { + return 1; + } + + return -1; +} + +export function sortChannelsByRecency(lastPosts: RelationOneToOne, a: Channel, b: Channel): number { + let aLastPostAt = a.last_post_at; + if (lastPosts[a.id] && lastPosts[a.id].create_at > a.last_post_at) { + aLastPostAt = lastPosts[a.id].create_at; + } + + let bLastPostAt = b.last_post_at; + if (lastPosts[b.id] && lastPosts[b.id].create_at > b.last_post_at) { + bLastPostAt = lastPosts[b.id].create_at; + } + + return bLastPostAt - aLastPostAt; +} + +export function isChannelMuted(member: ChannelMembership): boolean { + return member && member.notify_props ? (member.notify_props.mark_unread === 'mention') : false; +} + +export function compareNotifyProps(propsA: Partial, propsB: Partial): boolean { + if (propsA.desktop !== propsB.desktop || + propsA.email !== propsB.email || + propsA.mark_unread !== propsB.mark_unread || + propsA.push !== propsB.push || + propsA.ignore_channel_mentions !== propsB.ignore_channel_mentions) { + return false; + } + + return true; +} + +export function areChannelMentionsIgnored(channelMemberNotifyProps: ChannelNotifyProps, currentUserNotifyProps: UserNotifyProps) { + let ignoreChannelMentionsDefault = Users.IGNORE_CHANNEL_MENTIONS_OFF; + + if (currentUserNotifyProps.channel && currentUserNotifyProps.channel === 'false') { + ignoreChannelMentionsDefault = Users.IGNORE_CHANNEL_MENTIONS_ON; + } + + let ignoreChannelMentions = channelMemberNotifyProps && channelMemberNotifyProps.ignore_channel_mentions; + if (!ignoreChannelMentions || ignoreChannelMentions === Users.IGNORE_CHANNEL_MENTIONS_DEFAULT) { + ignoreChannelMentions = ignoreChannelMentionsDefault as any; + } + + return ignoreChannelMentions !== Users.IGNORE_CHANNEL_MENTIONS_OFF; +} + +function buildChannels(usersState: UsersState, channels: Array, missingDirectChannels: Array, teammateNameDisplay: string, locale: string): Array { + return channels. + concat(missingDirectChannels). + map((c) => completeDirectChannelInfo(usersState, teammateNameDisplay, c)). + filter(isNotDeletedChannel). + sort(sortChannelsByTypeAndDisplayName.bind(null, locale)); +} + +function buildFavoriteChannels(channels: Array, myPreferences: { + [x: string]: PreferenceType; +}, locale: string): Array { + return channels.filter((channel) => isFavoriteChannel(myPreferences, channel.id)).sort(sortChannelsByDisplayName.bind(null, locale)); +} + +function buildNotFavoriteChannels(channels: Array, myPreferences: { + [x: string]: PreferenceType; +}): Array { + return channels.filter((channel) => !isFavoriteChannel(myPreferences, channel.id)); +} + +function buildDirectAndGroupChannels(channels: Array, memberships: RelationOneToOne, config: any, myPreferences: { + [x: string]: PreferenceType; +}, currentUserId: string, users: IDMappedObjects, lastPosts: RelationOneToOne): Array { + return channels.filter((channel) => { + return isGroupOrDirectChannelVisible(channel, memberships, config, myPreferences, currentUserId, users, lastPosts); + }); +} + +function buildChannelsWithMentions(channels: Array, members: RelationOneToOne, locale: string) { + return channels.filter(channelHasMentions.bind(null, members)). + sort(sortChannelsByDisplayName.bind(null, locale)); +} + +function buildUnreadChannels(channels: Array, members: RelationOneToOne, locale: string) { + return channels.filter(channelHasUnreadMessages.bind(null, members)). + sort(sortChannelsByDisplayName.bind(null, locale)); +} + +function getUserLocale(userId: string, profiles: IDMappedObjects) { + let locale = General.DEFAULT_LOCALE; + if (profiles && profiles[userId] && profiles[userId].locale) { + locale = profiles[userId].locale; + } + + return locale; +} + +export function filterChannelsMatchingTerm(channels: Array, term: string): Array { + const lowercasedTerm = term.toLowerCase(); + + return channels.filter((channel: Channel): boolean => { + if (!channel) { + return false; + } + const name = (channel.name || '').toLowerCase(); + const displayName = (channel.display_name || '').toLowerCase(); + + return name.startsWith(lowercasedTerm) || + displayName.startsWith(lowercasedTerm); + }); +} diff --git a/app/mm-redux/utils/deep_freeze.ts b/app/mm-redux/utils/deep_freeze.ts new file mode 100644 index 000000000..2fc351cb5 --- /dev/null +++ b/app/mm-redux/utils/deep_freeze.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +/* eslint-disable header/header */ + +/** + * If your application is accepting different values for the same field over + * time and is doing a diff on them, you can either (1) create a copy or + * (2) ensure that those values are not mutated behind two passes. + * This function helps you with (2) by freezing the object and throwing if + * the user subsequently modifies the value. + * + * There are two caveats with this function: + * - If the call site is not in strict mode, it will only throw when + * mutating existing fields, adding a new one + * will unfortunately fail silently :( + * - If the object is already frozen or sealed, it will not continue the + * deep traversal and will leave leaf nodes unfrozen. + * + * Freezing the object and adding the throw mechanism is expensive and will + * only be used in DEV. + */ + +export default function deepFreezeAndThrowOnMutation(object: any): any { + // Some objects in IE11 don't have a hasOwnProperty method so don't even bother trying to freeze them + + if (typeof object !== 'object' || object === null || Object.isFrozen(object) || Object.isSealed(object)) { + return object; + } + + for (const key in object) { + if (object.hasOwnProperty(key)) { + object.__defineGetter__(key, identity.bind(null, object[key])); // eslint-disable-line no-underscore-dangle + object.__defineSetter__(key, throwOnImmutableMutation.bind(null, key)); // eslint-disable-line no-underscore-dangle + } + } + + Object.freeze(object); + Object.seal(object); + + for (const key in object) { + if (object.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutation(object[key]); + } + } + + return object; +} + +function throwOnImmutableMutation(key: string, value: any) { + throw Error( + 'You attempted to set the key `' + key + '` with the value `' + + JSON.stringify(value) + '` on an object that is meant to be immutable ' + + 'and has been frozen.' + ); +} + +function identity(value: any): any { + return value; +} diff --git a/app/mm-redux/utils/delayed_action.ts b/app/mm-redux/utils/delayed_action.ts new file mode 100644 index 000000000..64a3e3b83 --- /dev/null +++ b/app/mm-redux/utils/delayed_action.ts @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default class DelayedAction { + action: Function; + timer: NodeJS.Timeout | null; + + constructor(action: Function) { + this.action = action; + + this.timer = null; + } + + fire = (): void => { + this.action(); + + this.timer = null; + } + + fireAfter = (timeout: number): void => { + if (this.timer !== null) { + clearTimeout(this.timer); + } + + this.timer = setTimeout(this.fire, timeout); + } + + cancel = (): void => { + if (this.timer !== null) { + clearTimeout(this.timer); + } + } +} diff --git a/app/mm-redux/utils/emoji_utils.test.js b/app/mm-redux/utils/emoji_utils.test.js new file mode 100644 index 000000000..0fbd442bf --- /dev/null +++ b/app/mm-redux/utils/emoji_utils.test.js @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import * as EmojiUtils from '@mm-redux/utils/emoji_utils'; + +describe('EmojiUtils', () => { + describe('parseNeededCustomEmojisFromText', () => { + it('no emojis', () => { + const actual = EmojiUtils.parseNeededCustomEmojisFromText( + 'This has no emojis', + new Map(), + new Map(), + new Map(), + ); + const expected = new Set([]); + + assert.deepEqual(actual, expected); + }); + + it('some emojis', () => { + const actual = EmojiUtils.parseNeededCustomEmojisFromText( + ':this: :is_all: :emo-jis: :123:', + new Map(), + new Map(), + new Map(), + ); + const expected = new Set(['this', 'is_all', 'emo-jis', '123']); + + assert.deepEqual(actual, expected); + }); + + it('text surrounding emojis', () => { + const actual = EmojiUtils.parseNeededCustomEmojisFromText( + ':this:/:is_all: (:emo-jis:) surrounding:123:text:456:asdf', + new Map(), + new Map(), + new Map(), + ); + const expected = new Set(['this', 'is_all', 'emo-jis', '123', '456']); + + assert.deepEqual(actual, expected); + }); + + it('system emojis', () => { + const actual = EmojiUtils.parseNeededCustomEmojisFromText( + ':this: :is_all: :emo-jis: :123:', + new Map([['this', {name: 'this'}], ['123', {name: '123'}]]), + new Map(), + new Map(), + ); + const expected = new Set(['is_all', 'emo-jis']); + + assert.deepEqual(actual, expected); + }); + + it('custom emojis', () => { + const actual = EmojiUtils.parseNeededCustomEmojisFromText( + ':this: :is_all: :emo-jis: :123:', + new Map(), + new Map([['is_all', {name: 'is_all'}], ['emo-jis', {name: 'emo-jis'}]]), + new Map(), + ); + const expected = new Set(['this', '123']); + + assert.deepEqual(actual, expected); + }); + + it('emojis that we\'ve already tried to load', () => { + const actual = EmojiUtils.parseNeededCustomEmojisFromText( + ':this: :is_all: :emo-jis: :123:', + new Map(), + new Map(), + new Map([['this', {name: 'this'}], ['emo-jis', {name: 'emo-jis'}]]), + ); + const expected = new Set(['is_all', '123']); + + assert.deepEqual(actual, expected); + }); + }); +}); diff --git a/app/mm-redux/utils/emoji_utils.ts b/app/mm-redux/utils/emoji_utils.ts new file mode 100644 index 000000000..39d6d081b --- /dev/null +++ b/app/mm-redux/utils/emoji_utils.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4} from '@mm-redux/client'; +import {Emoji, SystemEmoji, CustomEmoji} from '@mm-redux/types/emojis'; + +export function isSystemEmoji(emoji: Emoji): emoji is SystemEmoji { + return 'batch' in emoji; +} + +export function isCustomEmoji(emoji: Emoji): emoji is CustomEmoji { + return 'id' in emoji; +} + +export function getEmojiImageUrl(emoji: Emoji): string { + if (isCustomEmoji(emoji)) { + return Client4.getEmojiRoute(emoji.id) + '/image'; + } + + const filename = emoji.filename || emoji.aliases[0]; + return Client4.getSystemEmojiImageUrl(filename); +} + +export function parseNeededCustomEmojisFromText(text: string, systemEmojis: Map, customEmojisByName: Map, nonExistentEmoji: Set): Set { + if (!text.includes(':')) { + return new Set(); + } + + const pattern = /:([A-Za-z0-9_-]+):/gi; + const customEmojis = new Set(); + let match; + while ((match = pattern.exec(text)) !== null) { + if (!match) { + continue; + } + + if (systemEmojis.has(match[1])) { + // It's a system emoji, go the next match + continue; + } + + if (nonExistentEmoji.has(match[1])) { + // We've previously confirmed this is not a custom emoji + continue; + } + + if (customEmojisByName.has(match[1])) { + // We have the emoji, go to the next match + continue; + } + + customEmojis.add(match[1]); + } + + return customEmojis; +} diff --git a/app/mm-redux/utils/event_emitter.ts b/app/mm-redux/utils/event_emitter.ts new file mode 100644 index 000000000..171e422da --- /dev/null +++ b/app/mm-redux/utils/event_emitter.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +function isFunction(obj: any): boolean { + return typeof obj === 'function'; +} + +class EventEmitter { + listeners: Map>; + + constructor() { + this.listeners = new Map(); + } + + addListener(label: string, callback: Function): void { + if (!this.listeners.has(label)) { + this.listeners.set(label, []); + } + + this.listeners.get(label)!.push(callback); + } + + on(label: string, callback: Function): void { + this.addListener(label, callback); + } + + removeListener(label: string, callback: Function): boolean { + const listeners = this.listeners.get(label); + let index; + + if (listeners && listeners.length) { + index = listeners.reduce((i, listener, idx) => { + return isFunction(listener) && listener === callback ? idx : i; + }, -1); + + if (index > -1) { + listeners.splice(index, 1); + this.listeners.set(label, listeners); + return true; + } + } + return false; + } + + off(label: string, callback: Function): void { + this.removeListener(label, callback); + } + + emit(label: string, ...args: Array): boolean { + const listeners = this.listeners.get(label); + + if (listeners && listeners.length) { + listeners.forEach((listener) => { + listener(...args); + }); + return true; + } + return false; + } +} + +export default new EventEmitter(); diff --git a/app/mm-redux/utils/file_utils.test.js b/app/mm-redux/utils/file_utils.test.js new file mode 100644 index 000000000..b68fa5127 --- /dev/null +++ b/app/mm-redux/utils/file_utils.test.js @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import {Client4} from '@mm-redux/client'; + +import * as FileUtils from '@mm-redux/utils/file_utils'; + +describe('FileUtils', () => { + const serverUrl = Client4.getUrl(); + beforeEach(() => { + Client4.setUrl('localhost'); + }); + + afterEach(() => { + Client4.setUrl(serverUrl); + }); + + it('getFileUrl', () => { + assert.deepEqual(FileUtils.getFileUrl('id1'), 'localhost/api/v4/files/id1'); + assert.deepEqual(FileUtils.getFileUrl('id2'), 'localhost/api/v4/files/id2'); + }); + + it('getFileDownloadUrl', () => { + assert.deepEqual(FileUtils.getFileDownloadUrl('id1'), 'localhost/api/v4/files/id1?download=1'); + assert.deepEqual(FileUtils.getFileDownloadUrl('id2'), 'localhost/api/v4/files/id2?download=1'); + }); + + it('getFileThumbnailUrl', () => { + assert.deepEqual(FileUtils.getFileThumbnailUrl('id1'), 'localhost/api/v4/files/id1/thumbnail'); + assert.deepEqual(FileUtils.getFileThumbnailUrl('id2'), 'localhost/api/v4/files/id2/thumbnail'); + }); + + it('getFilePreviewUrl', () => { + assert.deepEqual(FileUtils.getFilePreviewUrl('id1'), 'localhost/api/v4/files/id1/preview'); + assert.deepEqual(FileUtils.getFilePreviewUrl('id2'), 'localhost/api/v4/files/id2/preview'); + }); + + it('sortFileInfos', () => { + const testCases = [ + {inputFileInfos: [{name: 'aaa', create_at: 100}, {name: 'bbb', create_at: 200}], outputFileInfos: [{name: 'aaa', create_at: 100}, {name: 'bbb', create_at: 200}]}, + {inputFileInfos: [{name: 'bbb', create_at: 200}, {name: 'aaa', create_at: 100}], outputFileInfos: [{name: 'aaa', create_at: 100}, {name: 'bbb', create_at: 200}]}, + {inputFileInfos: [{name: 'aaa', create_at: 100}, {name: 'bbb', create_at: 200}, {name: 'ccc', create_at: 300}], outputFileInfos: [{name: 'aaa', create_at: 100}, {name: 'bbb', create_at: 200}, {name: 'ccc', create_at: 300}]}, + {inputFileInfos: [{name: 'ccc', create_at: 300}, {name: 'bbb', create_at: 200}, {name: 'aaa', create_at: 100}], outputFileInfos: [{name: 'aaa', create_at: 100}, {name: 'bbb', create_at: 200}, {name: 'ccc', create_at: 300}]}, + {inputFileInfos: [{id: '1', name: 'aaa', create_at: 100}, {id: '2', name: 'aaa', create_at: 200}], outputFileInfos: [{id: '1', name: 'aaa', create_at: 100}, {id: '2', name: 'aaa', create_at: 200}]}, + {inputFileInfos: [{id: '2', name: 'aaa', create_at: 200}, {id: '1', name: 'aaa', create_at: 100}], outputFileInfos: [{id: '1', name: 'aaa', create_at: 100}, {id: '2', name: 'aaa', create_at: 200}]}, + ]; + + testCases.forEach((testCase) => { + assert.deepEqual(FileUtils.sortFileInfos(testCase.inputFileInfos), testCase.outputFileInfos); + }); + }); + + it('lookupMimeType', () => { + const jpgFilePaths = ['file://aaa.jpg', 'file://bbb.JPG']; + const mimeType = 'image/jpeg'; + + jpgFilePaths.forEach((filePath) => { + assert.equal(FileUtils.lookupMimeType(filePath), mimeType); + }); + }); +}); diff --git a/app/mm-redux/utils/file_utils.ts b/app/mm-redux/utils/file_utils.ts new file mode 100644 index 000000000..4d4ca875b --- /dev/null +++ b/app/mm-redux/utils/file_utils.ts @@ -0,0 +1,101 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Files, General} from '../constants'; +import {Client4} from '@mm-redux/client'; +import {FileInfo} from '@mm-redux/types/files'; +import {Dictionary} from '@mm-redux/types/utilities'; + +const mimeDB = require('mime-db'); + +export function getFormattedFileSize(file: FileInfo): string { + const bytes = file.size; + const fileSizes = [ + ['TB', 1024 * 1024 * 1024 * 1024], + ['GB', 1024 * 1024 * 1024], + ['MB', 1024 * 1024], + ['KB', 1024], + ]; + const size = fileSizes.find((unitAndMinBytes) => { + const minBytes = unitAndMinBytes[1]; + return bytes > minBytes; + }); + + if (size) { + return `${Math.floor(bytes / (size[1] as any))} ${size[0]}`; + } + + return `${bytes} B`; +} + +export function getFileType(file: FileInfo): string { + if (!file || !file.extension) { + return 'other'; + } + + const fileExt = file.extension.toLowerCase(); + const fileTypes = [ + 'image', + 'code', + 'pdf', + 'video', + 'audio', + 'spreadsheet', + 'text', + 'word', + 'presentation', + 'patch', + ]; + return fileTypes.find((fileType) => { + const constForFileTypeExtList = `${fileType}_types`.toUpperCase(); + const fileTypeExts = Files[constForFileTypeExtList]; + return fileTypeExts.indexOf(fileExt) > -1; + }) || 'other'; +} + +let extToMime: Dictionary; +function buildExtToMime() { + extToMime = {}; + Object.keys(mimeDB).forEach((key) => { + const mime = mimeDB[key]; + if (mime.extensions) { + mime.extensions.forEach((ext: string) => { + extToMime[ext] = key; + }); + } + }); +} + +export function lookupMimeType(filename: string): string { + if (!extToMime) { + buildExtToMime(); + } + + const ext = filename.split('.').pop()!.toLowerCase(); + return extToMime[ext] || 'application/octet-stream'; +} + +export function getFileUrl(fileId: string): string { + return Client4.getFileRoute(fileId); +} + +export function getFileDownloadUrl(fileId: string): string { + return `${Client4.getFileRoute(fileId)}?download=1`; +} + +export function getFileThumbnailUrl(fileId: string): string { + return `${Client4.getFileRoute(fileId)}/thumbnail`; +} + +export function getFilePreviewUrl(fileId: string): string { + return `${Client4.getFileRoute(fileId)}/preview`; +} + +export function sortFileInfos(fileInfos: Array = [], locale: string = General.DEFAULT_LOCALE): Array { + return fileInfos.sort((a, b) => { + if (a.create_at !== b.create_at) { + return a.create_at - b.create_at; + } + + return a.name.localeCompare(b.name, locale, {numeric: true}); + }); +} diff --git a/app/mm-redux/utils/gfycat_sdk.ts b/app/mm-redux/utils/gfycat_sdk.ts new file mode 100644 index 000000000..fe3ce2433 --- /dev/null +++ b/app/mm-redux/utils/gfycat_sdk.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import Gfycat from 'gfycat-sdk'; +const defaultKey = '2_KtH_W5'; +const defaultSecret = '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof'; +let activeKey: string|null = null; +let activeSecret: string | null = null; + +let instance: any = null; +export default function(key: string, secret: string): any { + if (instance && activeKey === key && activeSecret === secret) { + return instance; + } + + if (!key || !secret) { + instance = new Gfycat({client_id: defaultKey, client_secret: defaultSecret}); + return instance; + } + + activeKey = key; + activeSecret = secret; + instance = new Gfycat({client_id: key, client_secret: secret}); + return instance; +} diff --git a/app/mm-redux/utils/helpers.test.js b/app/mm-redux/utils/helpers.test.js new file mode 100644 index 000000000..7a39cdcb1 --- /dev/null +++ b/app/mm-redux/utils/helpers.test.js @@ -0,0 +1,114 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {buildQueryString, isMinimumServerVersion, isEmail} from '@mm-redux/utils/helpers'; + +describe('Helpers', () => { + it('isMinimumServerVersion', () => { + assert.ok(isMinimumServerVersion('1.0.0', 1, 0, 0)); + assert.ok(isMinimumServerVersion('1.1.1', 1, 1, 1)); + assert.ok(!isMinimumServerVersion('1.0.0', 2, 0, 0)); + assert.ok(isMinimumServerVersion('4.6', 2, 0, 0)); + assert.ok(!isMinimumServerVersion('4.6', 4, 7, 0)); + assert.ok(isMinimumServerVersion('4.6.1', 2, 0, 0)); + assert.ok(isMinimumServerVersion('4.7.1', 4, 6, 2)); + assert.ok(!isMinimumServerVersion('4.6.1', 4, 6, 2)); + assert.ok(!isMinimumServerVersion('3.6.1', 4, 6, 2)); + assert.ok(isMinimumServerVersion('4.6.1', 3, 7, 2)); + assert.ok(isMinimumServerVersion('5', 4, 6, 2)); + assert.ok(isMinimumServerVersion('5', 5)); + assert.ok(isMinimumServerVersion('5.1', 5)); + assert.ok(isMinimumServerVersion('5.1', 5, 1)); + assert.ok(!isMinimumServerVersion('5.1', 5, 2)); + assert.ok(isMinimumServerVersion('5.1.0', 5)); + assert.ok(isMinimumServerVersion('5.1.1', 5, 1, 1)); + assert.ok(!isMinimumServerVersion('5.1.1', 5, 1, 2)); + assert.ok(isMinimumServerVersion('4.6.2.sakjdgaksfg', 4, 6, 2)); + assert.ok(!isMinimumServerVersion()); + }); + + it('buildQueryString', () => { + assert.equal(buildQueryString({}), ''); + assert.equal(buildQueryString({a: 1}), '?a=1'); + assert.equal(buildQueryString({a: 1, b: 'str'}), '?a=1&b=str'); + }); +}); + +describe('Utils.isEmail', () => { + it('', () => { + for (const data of [ + { + email: 'prettyandsimple@example.com', + valid: true, + }, + { + email: 'very.common@example.com', + valid: true, + }, + { + email: 'disposable.style.email.with+symbol@example.com', + valid: true, + }, + { + email: 'other.email-with-dash@example.com', + valid: true, + }, + { + email: 'fully-qualified-domain@example.com', + valid: true, + }, + { + email: 'user.name+tag+sorting@example.com', + valid: true, + }, + { + email: 'x@example.com', + valid: true, + }, + { + email: 'example-indeed@strange-example.com', + valid: true, + }, + { + email: 'admin@mailserver1', + valid: true, + }, + { + email: '#!$%&\'*+-/=?^_`{}|~@example.org', + valid: true, + }, + { + email: 'example@s.solutions', + valid: true, + }, + { + email: 'Abc.example.com', + valid: false, + }, + { + email: 'A@b@c@example.com', + valid: false, + }, + { + email: ' test.email@example.com', + valid: false, + }, + { + email: 'test ', + valid: false, + }, + { + email: 'comma@domain.com, separated@domain.com', + valid: false, + }, + { + email: 'comma@domain.com,separated@domain.com', + valid: false, + }, + ]) { + assert.equal(isEmail(data.email), data.valid); + } + }); +}); diff --git a/app/mm-redux/utils/helpers.ts b/app/mm-redux/utils/helpers.ts new file mode 100644 index 000000000..318a12be7 --- /dev/null +++ b/app/mm-redux/utils/helpers.ts @@ -0,0 +1,123 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import * as reselect from 'reselect'; +import shallowEqual from 'shallow-equals'; +import {Dictionary} from '@mm-redux/types/utilities'; +export function memoizeResult(func: F): any { + let lastArgs: IArguments|null = null; + let lastResult: any = null; + + // we reference arguments instead of spreading them for performance reasons + return function shallowCompare() { + if (!shallowEqual(lastArgs, arguments)) { //eslint-disable-line prefer-rest-params + //eslint-disable-line prefer-rest-params + // apply arguments instead of spreading for performance. + const result = Reflect.apply(func, null, arguments); //eslint-disable-line prefer-rest-params + if (!shallowEqual(lastResult, result)) { + lastResult = result; + } + } + + lastArgs = arguments; //eslint-disable-line prefer-rest-params + return lastResult; + }; +} + +// Use this selector when you want a shallow comparison of the arguments and you want to memoize the result +// try and use this only when your selector returns an array of ids +export const createIdsSelector = reselect.createSelectorCreator(memoizeResult); + +// Use this selector when you want a shallow comparison of the arguments and you don't need to memoize the result +export const createShallowSelector = reselect.createSelectorCreator(reselect.defaultMemoize, shallowEqual as any); + +// isMinimumServerVersion will return true if currentVersion is equal to higher or than the +// the provided minimum version. A non-equal major version will ignore minor and dot +// versions, and a non-equal minor version will ignore dot version. +// currentVersion is a string, e.g '4.6.0' +// minMajorVersion, minMinorVersion, minDotVersion are integers +export const isMinimumServerVersion = (currentVersion: string, minMajorVersion = 0, minMinorVersion = 0, minDotVersion = 0): boolean => { + if (!currentVersion || typeof currentVersion !== 'string') { + return false; + } + + const split = currentVersion.split('.'); + + const major = parseInt(split[0], 10); + const minor = parseInt(split[1] || '0', 10); + const dot = parseInt(split[2] || '0', 10); + + if (major > minMajorVersion) { + return true; + } + if (major < minMajorVersion) { + return false; + } + + // Major version is equal, check minor + if (minor > minMinorVersion) { + return true; + } + if (minor < minMinorVersion) { + return false; + } + + // Minor version is equal, check dot + if (dot > minDotVersion) { + return true; + } + if (dot < minDotVersion) { + return false; + } + + // Dot version is equal + return true; +}; + +// Generates a RFC-4122 version 4 compliant globally unique identifier. +export function generateId(): string { + // implementation taken from http://stackoverflow.com/a/2117523 + let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + id = id.replace(/[xy]/g, (c) => { + const r = Math.floor(Math.random() * 16); + let v; + + if (c === 'x') { + v = r; + } else { + // eslint-disable-next-line no-mixed-operators + v = r & 0x3 | 0x8; + } + + return v.toString(16); + }); + return id; +} + +export function isEmail(email: string): boolean { + // writing a regex to match all valid email addresses is really, really hard. (see http://stackoverflow.com/a/201378) + // this regex ensures: + // - at least one character that is not a space, comma, or @ symbol + // - followed by a single @ symbol + // - followed by at least one character that is not a space, comma, or @ symbol + // this prevents outlook.style@domain.com addresses and multiple comma-separated addresses from being accepted + return (/^[^ ,@]+@[^ ,@]+$/).test(email); +} + +export function buildQueryString(parameters: Dictionary): string { + const keys = Object.keys(parameters); + if (keys.length === 0) { + return ''; + } + + let query = '?'; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + query += key + '=' + encodeURIComponent(parameters[key]); + + if (i < keys.length - 1) { + query += '&'; + } + } + + return query; +} diff --git a/app/mm-redux/utils/i18n_utils.test.js b/app/mm-redux/utils/i18n_utils.test.js new file mode 100644 index 000000000..cb133a863 --- /dev/null +++ b/app/mm-redux/utils/i18n_utils.test.js @@ -0,0 +1,29 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {setLocalizeFunction, localizeMessage} from '@mm-redux/utils/i18n_utils'; +const en = require('assets/i18n/en.json'); +const [testKey, testValue] = Object.entries(en)[0]; + +describe('i18n utils', () => { + afterEach(() => { + setLocalizeFunction(null); + }); + + it('should return default message', () => { + assert.equal(localizeMessage(testKey, testValue), testValue); + }); + + it('should return previously set Localized function return value', () => { + const prevValue = 'previous-value'; + function mockFunc() { + return prevValue; + } + + setLocalizeFunction(mockFunc); + assert.notEqual(testValue, prevValue); + assert.equal(localizeMessage(testKey, testValue), prevValue); + }); +}); diff --git a/app/mm-redux/utils/i18n_utils.ts b/app/mm-redux/utils/i18n_utils.ts new file mode 100644 index 000000000..e18c803d8 --- /dev/null +++ b/app/mm-redux/utils/i18n_utils.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +let localizeFunction: Function|null = null; +export function setLocalizeFunction(func: Function) { + localizeFunction = func; +} + +export function localizeMessage(id: string, defaultMessage: string): string { + if (!localizeFunction) { + return defaultMessage; + } + + return localizeFunction(id, defaultMessage); +} diff --git a/app/mm-redux/utils/index.ts b/app/mm-redux/utils/index.ts new file mode 100644 index 000000000..52f502ff8 --- /dev/null +++ b/app/mm-redux/utils/index.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import * as channel_utils from './channel_utils'; +import * as deep_freeze from './deep_freeze'; +import * as delayed_action from './delayed_action'; +import * as emoji_utils from './emoji_utils'; +import * as event_emitter from './event_emitter'; +import * as file_utils from './file_utils'; +import * as gfycat_sdk from './gfycat_sdk'; +import * as helpers from './helpers'; +import * as i18n_utils from './i18n_utils'; +import * as integration_utils from './integration_utils'; +import * as key_mirror from './key_mirror'; +import * as notify_props from './notify_props'; +import * as post_list from './post_list'; +import * as post_utils from './post_utils'; +import * as preference_utils from './preference_utils'; +import * as sentry from './sentry'; +import * as team_utils from './team_utils'; +import * as theme_utils from './theme_utils'; +import * as timezone_utils from './timezone_utils'; +import * as user_utils from './user_utils'; + +export { + channel_utils, + deep_freeze, + delayed_action, + emoji_utils, + event_emitter, + file_utils, + gfycat_sdk, + helpers, + i18n_utils, + integration_utils, + key_mirror, + notify_props, + post_list, + post_utils, + preference_utils, + sentry, + team_utils, + theme_utils, + timezone_utils, + user_utils, +}; diff --git a/app/mm-redux/utils/integration_utils.test.js b/app/mm-redux/utils/integration_utils.test.js new file mode 100644 index 000000000..580f27a8c --- /dev/null +++ b/app/mm-redux/utils/integration_utils.test.js @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; +import {checkDialogElementForError, checkIfErrorsMatchElements} from '@mm-redux/utils/integration_utils'; + +describe('integration utils', () => { + describe('checkDialogElementForError', () => { + it('should return null error on optional text element', () => { + assert.equal(checkDialogElementForError({type: 'text', optional: true}), null); + }); + + it('should return null error on optional textarea element', () => { + assert.equal(checkDialogElementForError({type: 'textarea', optional: true}), null); + }); + + it('should return error on required element', () => { + assert.equal(checkDialogElementForError({type: 'text', optional: false}).id, 'interactive_dialog.error.required'); + }); + + it('should return error on too short text element', () => { + assert.equal(checkDialogElementForError({type: 'text', min_length: 5}, '123').id, 'interactive_dialog.error.too_short'); + }); + + it('should return null on good number element', () => { + assert.equal(checkDialogElementForError({type: 'text', subtype: 'number'}, '123'), null); + }); + + it('should return error on bad number element', () => { + assert.equal(checkDialogElementForError({type: 'text', subtype: 'number'}, 'totallyanumber').id, 'interactive_dialog.error.bad_number'); + }); + + it('should return null on good email element', () => { + assert.equal(checkDialogElementForError({type: 'text', subtype: 'email'}, 'joram@mattermost.com'), null); + }); + + it('should return error on bad email element', () => { + assert.equal(checkDialogElementForError({type: 'text', subtype: 'email'}, 'totallyanemail').id, 'interactive_dialog.error.bad_email'); + }); + + it('should return null on good url element', () => { + assert.equal(checkDialogElementForError({type: 'text', subtype: 'url'}, 'http://mattermost.com'), null); + assert.equal(checkDialogElementForError({type: 'text', subtype: 'url'}, 'https://mattermost.com'), null); + }); + + it('should return error on bad url element', () => { + assert.equal(checkDialogElementForError({type: 'text', subtype: 'url'}, 'totallyawebsite').id, 'interactive_dialog.error.bad_url'); + }); + + it('should return null when value is in the options', () => { + assert.equal(checkDialogElementForError({type: 'radio', options: [{value: 'Sales'}]}, 'Sales'), null); + }); + + it('should return error when value is not in the options', () => { + assert.equal(checkDialogElementForError({type: 'radio', options: [{value: 'Sales'}]}, 'Sale').id, 'interactive_dialog.error.invalid_option'); + }); + + it('should return error when value is falsey and not on the list of options', () => { + assert.equal(checkDialogElementForError({type: 'radio', options: [{value: false}]}, 'Sale').id, 'interactive_dialog.error.invalid_option'); + + assert.equal(checkDialogElementForError({type: 'radio', options: [{value: undefined}]}, 'Sale').id, 'interactive_dialog.error.invalid_option'); + + assert.equal(checkDialogElementForError({type: 'radio', options: [{value: null}]}, 'Sale').id, 'interactive_dialog.error.invalid_option'); + }); + }); + + describe('checkIfErrorsMatchElements', () => { + it('should pass as returned error matches an element', () => { + assert.ok(checkIfErrorsMatchElements({name1: 'some error'}, [{name: 'name1'}])); + assert.ok(checkIfErrorsMatchElements({name1: 'some error'}, [{name: 'name1'}, {name: 'name2'}])); + }); + + it('should fail as returned errors do not match an element', () => { + assert.ok(!checkIfErrorsMatchElements({name17: 'some error'}, [{name: 'name1'}, {name: 'name2'}])); + }); + }); +}); diff --git a/app/mm-redux/utils/integration_utils.ts b/app/mm-redux/utils/integration_utils.ts new file mode 100644 index 000000000..a563b6d92 --- /dev/null +++ b/app/mm-redux/utils/integration_utils.ts @@ -0,0 +1,87 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {DialogElement} from '@mm-redux/types/integrations'; + +type DialogError = { + id: string; + defaultMessage: string; + values?: any; +}; +export function checkDialogElementForError(elem: DialogElement, value: any): DialogError | undefined | null { + if (!value && !elem.optional) { + return { + id: 'interactive_dialog.error.required', + defaultMessage: 'This field is required.', + }; + } + + const type = elem.type; + + if (type === 'text' || type === 'textarea') { + if (value && value.length < elem.min_length) { + return { + id: 'interactive_dialog.error.too_short', + defaultMessage: 'Minimum input length is {minLength}.', + values: {minLength: elem.min_length}, + }; + } + + if (elem.subtype === 'email') { + if (value && !value.includes('@')) { + return { + id: 'interactive_dialog.error.bad_email', + defaultMessage: 'Must be a valid email address.', + }; + } + } + + if (elem.subtype === 'number') { + if (value && isNaN(value)) { + return { + id: 'interactive_dialog.error.bad_number', + defaultMessage: 'Must be a number.', + }; + } + } + + if (elem.subtype === 'url') { + if (value && !value.includes('http://') && !value.includes('https://')) { + return { + id: 'interactive_dialog.error.bad_url', + defaultMessage: 'URL must include http:// or https://.', + }; + } + } + } else if (type === 'radio') { + const options = elem.options; + + if (typeof value !== 'undefined' && Array.isArray(options) && !options.some((e) => e.value === value)) { + return { + id: 'interactive_dialog.error.invalid_option', + defaultMessage: 'Must be a valid option', + }; + } + } + + return null; +} + +// If we're returned errors that don't match any of the elements we have, +// ignore them and complete the dialog + +export function checkIfErrorsMatchElements(errors: { + [x: string]: DialogError; +} = {}, elements: Array = []) { + for (const name in errors) { + if (!errors.hasOwnProperty(name)) { + continue; + } + for (const elem of elements) { + if (elem.name === name) { + return true; + } + } + } + + return false; +} diff --git a/app/mm-redux/utils/key_mirror.ts b/app/mm-redux/utils/key_mirror.ts new file mode 100644 index 000000000..e8c98b44f --- /dev/null +++ b/app/mm-redux/utils/key_mirror.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +/* eslint-disable header/header */ + +/** + * Constructs an enumeration with keys equal to their value. + * + * For example: + * + * var COLORS = keyMirror({blue: null, red: null}); + * var myColor = COLORS.blue; + * var isColorValid = !!COLORS[myColor]; + * + * The last line could not be performed if the values of the generated enum were + * not equal to their keys. + * + * Input: {key1: val1, key2: val2} + * Output: {key1: key1, key2: key2} + * + * @param {object} obj + * @return {object} + */ +export default function keyMirror(obj: T): { [K in keyof T]: K } { + if (!(obj instanceof Object && !Array.isArray(obj))) { + throw new Error('keyMirror(...): Argument must be an object.'); + } + + const ret: any = {}; + for (const key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + + ret[key] = key; + } + return ret; +} diff --git a/app/mm-redux/utils/notify_props.test.js b/app/mm-redux/utils/notify_props.test.js new file mode 100644 index 000000000..5d629f7dc --- /dev/null +++ b/app/mm-redux/utils/notify_props.test.js @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {Preferences} from '../constants'; +import {getEmailInterval} from '@mm-redux/utils/notify_props'; + +describe('user utils', () => { + it('getEmailInterval should return correct interval', () => { + const testCases = [ + {enableEmail: false, enableBatching: true, intervalPreference: Preferences.INTERVAL_IMMEDIATE, out: Preferences.INTERVAL_NEVER}, + + {enableEmail: true, enableBatching: true, out: Preferences.INTERVAL_FIFTEEN_MINUTES}, + {enableEmail: true, enableBatching: true, intervalPreference: Preferences.INTERVAL_IMMEDIATE, out: Preferences.INTERVAL_IMMEDIATE}, + {enableEmail: true, enableBatching: true, intervalPreference: Preferences.INTERVAL_NEVER, out: Preferences.INTERVAL_IMMEDIATE}, + {enableEmail: true, enableBatching: true, intervalPreference: Preferences.INTERVAL_FIFTEEN_MINUTES, out: Preferences.INTERVAL_FIFTEEN_MINUTES}, + {enableEmail: true, enableBatching: true, intervalPreference: Preferences.INTERVAL_HOUR, out: Preferences.INTERVAL_HOUR}, + + {enableEmail: true, enableBatching: false, out: Preferences.INTERVAL_IMMEDIATE}, + {enableEmail: true, enableBatching: false, intervalPreference: Preferences.INTERVAL_IMMEDIATE, out: Preferences.INTERVAL_IMMEDIATE}, + {enableEmail: true, enableBatching: false, intervalPreference: Preferences.INTERVAL_NEVER, out: Preferences.INTERVAL_IMMEDIATE}, + {enableEmail: true, enableBatching: false, intervalPreference: Preferences.INTERVAL_FIFTEEN_MINUTES, out: Preferences.INTERVAL_IMMEDIATE}, + {enableEmail: true, enableBatching: false, intervalPreference: Preferences.INTERVAL_HOUR, out: Preferences.INTERVAL_IMMEDIATE}, + ]; + + testCases.forEach((testCase) => { + assert.equal(getEmailInterval(testCase.enableEmail, testCase.enableBatching, testCase.intervalPreference), testCase.out, `getEmailInterval(${testCase.enableEmail}, ${testCase.enableBatching}, ${testCase.intervalPreference}) should return ${testCase.out}`); + }); + }); +}); diff --git a/app/mm-redux/utils/notify_props.ts b/app/mm-redux/utils/notify_props.ts new file mode 100644 index 000000000..f54813d47 --- /dev/null +++ b/app/mm-redux/utils/notify_props.ts @@ -0,0 +1,29 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Preferences} from '../constants'; +export function getEmailInterval(enableEmailNotification: boolean, enableEmailBatching: number, emailIntervalPreference: number): number { + const { + INTERVAL_NEVER, + INTERVAL_IMMEDIATE, + INTERVAL_FIFTEEN_MINUTES, + INTERVAL_HOUR, + } = Preferences; + + const validValuesWithEmailBatching = [INTERVAL_IMMEDIATE, INTERVAL_NEVER, INTERVAL_FIFTEEN_MINUTES, INTERVAL_HOUR]; + const validValuesWithoutEmailBatching = [INTERVAL_IMMEDIATE, INTERVAL_NEVER]; + + if (!enableEmailNotification) { + return INTERVAL_NEVER; + } else if (enableEmailBatching && validValuesWithEmailBatching.indexOf(emailIntervalPreference) === -1) { + // When email batching is enabled, the default interval is 15 minutes + return INTERVAL_FIFTEEN_MINUTES; + } else if (!enableEmailBatching && validValuesWithoutEmailBatching.indexOf(emailIntervalPreference) === -1) { + // When email batching is not enabled, the default interval is immediately + return INTERVAL_IMMEDIATE; + } else if (enableEmailNotification && emailIntervalPreference === INTERVAL_NEVER) { + // When email notification is enabled, the default interval is immediately + return INTERVAL_IMMEDIATE; + } + + return emailIntervalPreference; +} diff --git a/app/mm-redux/utils/post_list.test.js b/app/mm-redux/utils/post_list.test.js new file mode 100644 index 000000000..2cecd5631 --- /dev/null +++ b/app/mm-redux/utils/post_list.test.js @@ -0,0 +1,1702 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {Posts, Preferences} from '../constants'; +import deepFreeze from '@mm-redux/utils/deep_freeze'; +import {getPreferenceKey} from '@mm-redux/utils/preference_utils'; + +import { + COMBINED_USER_ACTIVITY, + combineUserActivitySystemPost, + comparePostTypes, + DATE_LINE, + getDateForDateLine, + getFirstPostId, + getLastPostId, + getLastPostIndex, + getPostIdsForCombinedUserActivityPost, + isCombinedUserActivityPost, + isDateLine, + makeCombineUserActivityPosts, + makeFilterPostsAndAddSeparators, + makeGenerateCombinedPost, + postTypePriority, + START_OF_NEW_MESSAGES, +} from './post_list'; + +describe('makeFilterPostsAndAddSeparators', () => { + it('filter join/leave posts', () => { + const filterPostsAndAddSeparators = makeFilterPostsAndAddSeparators(); + const time = Date.now(); + const today = new Date(time); + + let state = { + entities: { + general: { + config: {}, + }, + posts: { + posts: { + 1001: {id: '1001', create_at: time, type: ''}, + 1002: {id: '1002', create_at: time + 1, type: Posts.POST_TYPES.JOIN_CHANNEL}, + }, + }, + preferences: { + myPreferences: {}, + }, + users: { + currentUserId: '1234', + profiles: { + 1234: {id: '1234', username: 'user'}, + }, + }, + }, + }; + const lastViewedAt = Number.POSITIVE_INFINITY; + const postIds = ['1002', '1001']; + const indicateNewMessages = true; + + // Defaults to show post + let now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages}); + assert.deepEqual(now, [ + '1002', + '1001', + 'date-' + today.getTime(), + ]); + + // Show join/leave posts + state = { + ...state, + entities: { + ...state.entities, + preferences: { + ...state.entities.preferences, + myPreferences: { + ...state.entities.preferences.myPreferences, + [getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: { + category: Preferences.CATEGORY_ADVANCED_SETTINGS, + name: Preferences.ADVANCED_FILTER_JOIN_LEAVE, + value: 'true', + }, + }, + }, + }, + }; + + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages}); + assert.deepEqual(now, [ + '1002', + '1001', + 'date-' + today.getTime(), + ]); + + // Hide join/leave posts + state = { + ...state, + entities: { + ...state.entities, + preferences: { + ...state.entities.preferences, + myPreferences: { + ...state.entities.preferences.myPreferences, + [getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: { + category: Preferences.CATEGORY_ADVANCED_SETTINGS, + name: Preferences.ADVANCED_FILTER_JOIN_LEAVE, + value: 'false', + }, + }, + }, + }, + }; + + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages}); + assert.deepEqual(now, [ + '1001', + 'date-' + today.getTime(), + ]); + + // always show join/leave posts for the current user + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + 1002: {id: '1002', create_at: time + 1, type: Posts.POST_TYPES.JOIN_CHANNEL, props: {username: 'user'}}, + }, + }, + }, + }; + + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages}); + + assert.deepEqual(now, [ + '1002', + '1001', + 'date-' + today.getTime(), + ]); + }); + + it('new messages indicator', () => { + const filterPostsAndAddSeparators = makeFilterPostsAndAddSeparators(); + const time = Date.now(); + const today = new Date(time); + + const state = { + entities: { + general: { + config: {}, + }, + posts: { + posts: { + 1000: {id: '1000', create_at: time + 1000, type: ''}, + 1005: {id: '1005', create_at: time + 1005, type: ''}, + 1010: {id: '1010', create_at: time + 1010, type: ''}, + }, + }, + preferences: { + myPreferences: {}, + }, + users: { + currentUserId: '1234', + profiles: { + 1234: {id: '1234', username: 'user'}, + }, + }, + }, + }; + + const postIds = ['1010', '1005', '1000']; // Remember that we list the posts backwards + + // Do not show new messages indicator before all posts + let now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt: 0, indicateNewMessages: true}); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-' + (today.getTime() + 1000), + ]); + + now = filterPostsAndAddSeparators(state, {postIds, indicateNewMessages: true}); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-' + (today.getTime() + 1000), + ]); + + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt: time + 999, indicateNewMessages: false}); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-' + (today.getTime() + 1000), + ]); + + // Show new messages indicator before all posts + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt: time + 999, indicateNewMessages: true}); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + START_OF_NEW_MESSAGES, + 'date-' + (today.getTime() + 1000), + ]); + + // Show indicator between posts + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt: time + 1003, indicateNewMessages: true}); + assert.deepEqual(now, [ + '1010', + '1005', + START_OF_NEW_MESSAGES, + '1000', + 'date-' + (today.getTime() + 1000), + ]); + + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt: time + 1006, indicateNewMessages: true}); + assert.deepEqual(now, [ + '1010', + START_OF_NEW_MESSAGES, + '1005', + '1000', + 'date-' + (today.getTime() + 1000), + ]); + + // Don't show indicator when all posts are read + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt: time + 1020}); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-' + (today.getTime() + 1000), + ]); + }); + + it('memoization', () => { + const filterPostsAndAddSeparators = makeFilterPostsAndAddSeparators(); + const time = Date.now(); + const today = new Date(time); + const tomorrow = new Date((24 * 60 * 60 * 1000) + today.getTime()); + + // Posts 7 hours apart so they should appear on multiple days + const initialPosts = { + 1001: {id: '1001', create_at: time, type: ''}, + 1002: {id: '1002', create_at: time + 5, type: ''}, + 1003: {id: '1003', create_at: time + 10, type: ''}, + 1004: {id: '1004', create_at: tomorrow, type: ''}, + 1005: {id: '1005', create_at: tomorrow + 5, type: ''}, + 1006: {id: '1006', create_at: tomorrow + 10, type: Posts.POST_TYPES.JOIN_CHANNEL}, + }; + let state = { + entities: { + general: { + config: {}, + }, + posts: { + posts: initialPosts, + }, + preferences: { + myPreferences: { + [getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: { + category: Preferences.CATEGORY_ADVANCED_SETTINGS, + name: Preferences.ADVANCED_FILTER_JOIN_LEAVE, + value: 'true', + }, + }, + }, + users: { + currentUserId: '1234', + profiles: { + 1234: {id: '1234', username: 'user'}, + }, + }, + }, + }; + + let postIds = [ + '1006', + '1004', + '1003', + '1001', + ]; + let lastViewedAt = initialPosts['1001'].create_at + 1; + + let now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.deepEqual(now, [ + '1006', + '1004', + 'date-' + tomorrow.getTime(), + '1003', + START_OF_NEW_MESSAGES, + '1001', + 'date-' + today.getTime(), + ]); + + // No changes + let prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + 'date-' + tomorrow.getTime(), + '1003', + START_OF_NEW_MESSAGES, + '1001', + 'date-' + today.getTime(), + ]); + + // lastViewedAt changed slightly + lastViewedAt = initialPosts['1001'].create_at + 2; + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + 'date-' + tomorrow.getTime(), + '1003', + START_OF_NEW_MESSAGES, + '1001', + 'date-' + today.getTime(), + ]); + + // lastViewedAt changed a lot + lastViewedAt = initialPosts['1003'].create_at + 1; + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.notEqual(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + + // postIds changed, but still shallowly equal + postIds = [...postIds]; + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + + // Post changed, not in postIds + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + 1007: {id: '1007', create_at: 7 * 60 * 60 * 7 * 1000}, + }, + }, + }, + }; + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + + // Post changed, in postIds + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + 1006: {...state.entities.posts.posts['1006'], message: 'abcd'}, + }, + }, + }, + }; + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + + // Filter changed + state = { + ...state, + entities: { + ...state.entities, + preferences: { + ...state.entities.preferences, + myPreferences: { + ...state.entities.preferences.myPreferences, + [getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: { + category: Preferences.CATEGORY_ADVANCED_SETTINGS, + name: Preferences.ADVANCED_FILTER_JOIN_LEAVE, + value: 'false', + }, + }, + }, + }, + }; + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.notEqual(now, prev); + assert.deepEqual(now, [ + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + + prev = now; + now = filterPostsAndAddSeparators(state, {postIds, lastViewedAt, indicateNewMessages: true}); + assert.equal(now, prev); + assert.deepEqual(now, [ + '1004', + START_OF_NEW_MESSAGES, + 'date-' + tomorrow.getTime(), + '1003', + '1001', + 'date-' + today.getTime(), + ]); + }); +}); + +describe('makeCombineUserActivityPosts', () => { + test('should do nothing if no post IDs are provided', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = []; + const state = { + entities: { + posts: { + posts: {}, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).toBe(postIds); + expect(result).toEqual([]); + }); + + test('should do nothing if there are no user activity posts', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = deepFreeze([ + 'post1', + START_OF_NEW_MESSAGES, + 'post2', + DATE_LINE + '1001', + 'post3', + DATE_LINE + '1000', + ]); + const state = { + entities: { + posts: { + posts: { + post1: {id: 'post1'}, + post2: {id: 'post2'}, + post3: {id: 'post3'}, + }, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).toBe(postIds); + }); + + test('should combine adjacent user activity posts', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = deepFreeze([ + 'post1', + 'post2', + 'post3', + ]); + const state = { + entities: { + posts: { + posts: { + post1: {id: 'post1', type: Posts.POST_TYPES.JOIN_CHANNEL}, + post2: {id: 'post2', type: Posts.POST_TYPES.LEAVE_CHANNEL}, + post3: {id: 'post3', type: Posts.POST_TYPES.ADD_TO_CHANNEL}, + }, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).not.toBe(postIds); + expect(result).toEqual([ + COMBINED_USER_ACTIVITY + 'post1_post2_post3', + ]); + }); + + test('should "combine" a single activity post', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = deepFreeze([ + 'post1', + 'post2', + 'post3', + ]); + const state = { + entities: { + posts: { + posts: { + post1: {id: 'post1'}, + post2: {id: 'post2', type: Posts.POST_TYPES.LEAVE_CHANNEL}, + post3: {id: 'post3'}, + }, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).not.toBe(postIds); + expect(result).toEqual([ + 'post1', + COMBINED_USER_ACTIVITY + 'post2', + 'post3', + ]); + }); + + test('should not combine with regular messages', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = deepFreeze([ + 'post1', + 'post2', + 'post3', + 'post4', + 'post5', + ]); + const state = { + entities: { + posts: { + posts: { + post1: {id: 'post1', type: Posts.POST_TYPES.JOIN_CHANNEL}, + post2: {id: 'post2', type: Posts.POST_TYPES.JOIN_CHANNEL}, + post3: {id: 'post3'}, + post4: {id: 'post4', type: Posts.POST_TYPES.ADD_TO_CHANNEL}, + post5: {id: 'post5', type: Posts.POST_TYPES.ADD_TO_CHANNEL}, + }, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).not.toBe(postIds); + expect(result).toEqual([ + COMBINED_USER_ACTIVITY + 'post1_post2', + 'post3', + COMBINED_USER_ACTIVITY + 'post4_post5', + ]); + }); + + test('should not combine with other system messages', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = deepFreeze([ + 'post1', + 'post2', + 'post3', + ]); + const state = { + entities: { + posts: { + posts: { + post1: {id: 'post1', type: Posts.POST_TYPES.JOIN_CHANNEL}, + post2: {id: 'post2', type: Posts.POST_TYPES.PURPOSE_CHANGE}, + post3: {id: 'post3', type: Posts.POST_TYPES.ADD_TO_CHANNEL}, + }, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).not.toBe(postIds); + expect(result).toEqual([ + COMBINED_USER_ACTIVITY + 'post1', + 'post2', + COMBINED_USER_ACTIVITY + 'post3', + ]); + }); + + test('should not combine across non-post items', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = deepFreeze([ + 'post1', + START_OF_NEW_MESSAGES, + 'post2', + 'post3', + DATE_LINE + '1001', + 'post4', + ]); + const state = { + entities: { + posts: { + posts: { + post1: {id: 'post1', type: Posts.POST_TYPES.JOIN_CHANNEL}, + post2: {id: 'post2', type: Posts.POST_TYPES.LEAVE_CHANNEL}, + post3: {id: 'post3', type: Posts.POST_TYPES.ADD_TO_CHANNEL}, + post4: {id: 'post4', type: Posts.POST_TYPES.JOIN_CHANNEL}, + }, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).not.toBe(postIds); + expect(result).toEqual([ + COMBINED_USER_ACTIVITY + 'post1', + START_OF_NEW_MESSAGES, + COMBINED_USER_ACTIVITY + 'post2_post3', + DATE_LINE + '1001', + COMBINED_USER_ACTIVITY + 'post4', + ]); + }); + + test('should not combine more than 100 posts', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + const postIds = []; + const posts = {}; + for (let i = 0; i < 110; i++) { + const postId = `post${i}`; + + postIds.push(postId); + posts[postId] = {id: postId, type: Posts.POST_TYPES.JOIN_CHANNEL}; + } + + const state = { + entities: { + posts: { + posts, + }, + }, + }; + + const result = combineUserActivityPosts(state, postIds); + + expect(result).toHaveLength(2); + }); + + describe('memoization', () => { + const initialPostIds = ['post1', 'post2']; + const initialState = { + entities: { + posts: { + posts: { + post1: {id: 'post1', type: Posts.POST_TYPES.JOIN_CHANNEL}, + post2: {id: 'post2', type: Posts.POST_TYPES.JOIN_CHANNEL}, + }, + }, + }, + }; + + test('should not recalculate when nothing has changed', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + expect(combineUserActivityPosts.recomputations()).toBe(0); + + combineUserActivityPosts(initialState, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(1); + + combineUserActivityPosts(initialState, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(1); + }); + + test('should recalculate when the post IDs change', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + let postIds = initialPostIds; + combineUserActivityPosts(initialState, postIds); + + expect(combineUserActivityPosts.recomputations()).toBe(1); + + postIds = ['post1']; + combineUserActivityPosts(initialState, postIds); + + expect(combineUserActivityPosts.recomputations()).toBe(2); + }); + + test('should not recalculate when an unrelated state change occurs', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + let state = initialState; + combineUserActivityPosts(state, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(1); + + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + selectedPostId: 'post2', + }, + }, + }; + combineUserActivityPosts(state, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(1); + }); + + test('should recalculate if any post changes, but should return the same results if possible', () => { + const combineUserActivityPosts = makeCombineUserActivityPosts(); + + let state = initialState; + const initialResult = combineUserActivityPosts(state, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(1); + + // An unrelated post changed + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + post3: {id: 'post3'}, + }, + }, + }, + }; + let result = combineUserActivityPosts(state, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(2); + expect(result).toBe(initialResult); + + // One of the posts was updated, but post type didn't change + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + post2: {...state.entities.posts.posts.post2, update_at: 1234}, + }, + }, + }, + }; + result = combineUserActivityPosts(state, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(3); + expect(result).toBe(initialResult); + + // One of the posts changed type + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + post2: {...state.entities.posts.posts.post2, type: ''}, + }, + }, + }, + }; + result = combineUserActivityPosts(state, initialPostIds); + + expect(combineUserActivityPosts.recomputations()).toBe(4); + expect(result).not.toBe(initialResult); + }); + }); +}); + +describe('isDateLine', () => { + test('should correctly identify date line items', () => { + expect(isDateLine('')).toBe(false); + expect(isDateLine('date')).toBe(false); + expect(isDateLine('date-')).toBe(true); + expect(isDateLine('date-0')).toBe(true); + expect(isDateLine('date-1531152392')).toBe(true); + expect(isDateLine('date-1531152392-index')).toBe(true); + }); +}); + +describe('getDateForDateLine', () => { + test('should get date correctly without suffix', () => { + expect(getDateForDateLine('date-1234')).toBe(1234); + }); + + test('should get date correctly with suffix', () => { + expect(getDateForDateLine('date-1234-suffix')).toBe(1234); + }); +}); + +describe('isCombinedUserActivityPost', () => { + test('should correctly identify combined user activity posts', () => { + expect(isCombinedUserActivityPost('post1')).toBe(false); + expect(isCombinedUserActivityPost('date-1234')).toBe(false); + expect(isCombinedUserActivityPost('user-activity-post1')).toBe(true); + expect(isCombinedUserActivityPost('user-activity-post1_post2')).toBe(true); + expect(isCombinedUserActivityPost('user-activity-post1_post2_post4')).toBe(true); + }); +}); + +describe('getPostIdsForCombinedUserActivityPost', () => { + test('should get IDs correctly', () => { + expect(getPostIdsForCombinedUserActivityPost('user-activity-post1_post2_post3')).toEqual(['post1', 'post2', 'post3']); + }); +}); + +describe('getFirstPostId', () => { + test('should return the first item if it is a post', () => { + expect(getFirstPostId(['post1', 'post2', 'post3'])).toBe('post1'); + }); + + test('should return the first ID from a combined post', () => { + expect(getFirstPostId(['user-activity-post2_post3', 'post4', 'user-activity-post5_post6'])).toBe('post2'); + }); + + test('should skip date separators', () => { + expect(getFirstPostId(['date-1234', 'user-activity-post1_post2', 'post3', 'post4', 'date-1000'])).toBe('post1'); + }); + + test('should skip the new message line', () => { + expect(getFirstPostId([START_OF_NEW_MESSAGES, 'post2', 'post3', 'post4'])).toBe('post2'); + }); +}); + +describe('getLastPostId', () => { + test('should return the last item if it is a post', () => { + expect(getLastPostId(['post1', 'post2', 'post3'])).toBe('post3'); + }); + + test('should return the last ID from a combined post', () => { + expect(getLastPostId(['user-activity-post2_post3', 'post4', 'user-activity-post5_post6'])).toBe('post6'); + }); + + test('should skip date separators', () => { + expect(getLastPostId(['date-1234', 'user-activity-post1_post2', 'post3', 'post4', 'date-1000'])).toBe('post4'); + }); + + test('should skip the new message line', () => { + expect(getLastPostId(['post2', 'post3', 'post4', START_OF_NEW_MESSAGES])).toBe('post4'); + }); +}); + +describe('getLastPostIndex', () => { + test('should return index of last post for list of all regular posts', () => { + expect(getLastPostIndex(['post1', 'post2', 'post3'])).toBe(2); + }); + + test('should return index of last combined post', () => { + expect(getLastPostIndex(['user-activity-post2_post3', 'post4', 'user-activity-post5_post6'])).toBe(2); + }); + + test('should skip date separators and return index of last post', () => { + expect(getLastPostIndex(['date-1234', 'user-activity-post1_post2', 'post3', 'post4', 'date-1000'])).toBe(3); + }); + + test('should skip the new message line and return index of last post', () => { + expect(getLastPostIndex(['post2', 'post3', 'post4', START_OF_NEW_MESSAGES])).toBe(2); + }); +}); + +describe('makeGenerateCombinedPost', () => { + test('should output a combined post', () => { + const generateCombinedPost = makeGenerateCombinedPost(); + + const state = { + entities: { + posts: { + posts: { + post1: { + id: 'post1', + channel_id: 'channel1', + create_at: 1002, + delete_at: 0, + message: 'joe added to the channel by bill.', + props: { + addedUsername: 'joe', + addedUserId: 'user2', + username: 'bill', + userId: 'user1', + }, + type: Posts.POST_TYPES.ADD_TO_CHANNEL, + user_id: 'user1', + metadata: {}, + }, + post2: { + id: 'post2', + channel_id: 'channel1', + create_at: 1001, + delete_at: 0, + message: 'alice added to the channel by bill.', + props: { + addedUsername: 'alice', + addedUserId: 'user3', + username: 'bill', + userId: 'user1', + }, + type: Posts.POST_TYPES.ADD_TO_CHANNEL, + user_id: 'user1', + metadata: {}, + }, + post3: { + id: 'post3', + channel_id: 'channel1', + create_at: 1000, + delete_at: 0, + message: 'bill joined the channel.', + props: { + username: 'bill', + userId: 'user1', + }, + type: Posts.POST_TYPES.JOIN_CHANNEL, + user_id: 'user1', + metadata: {}, + }, + }, + }, + }, + }; + const combinedId = 'user-activity-post1_post2_post3'; + + const result = generateCombinedPost(state, combinedId); + + expect(result).toEqual({ + id: combinedId, + root_id: '', + channel_id: 'channel1', + create_at: 1000, + delete_at: 0, + message: 'joe added to the channel by bill.\nalice added to the channel by bill.\nbill joined the channel.', + props: { + messages: [ + 'joe added to the channel by bill.', + 'alice added to the channel by bill.', + 'bill joined the channel.', + ], + user_activity: { + allUserIds: ['user2', 'user3', 'user1'], + allUsernames: [], + messageData: [ + { + postType: Posts.POST_TYPES.JOIN_CHANNEL, + userIds: ['user1'], + }, + { + postType: Posts.POST_TYPES.ADD_TO_CHANNEL, + userIds: ['user2', 'user3'], + actorId: 'user1', + }, + ], + }, + }, + state: '', + system_post_ids: ['post1', 'post2', 'post3'], + type: Posts.POST_TYPES.COMBINED_USER_ACTIVITY, + user_activity_posts: [ + state.entities.posts.posts.post1, + state.entities.posts.posts.post2, + state.entities.posts.posts.post3, + ], + user_id: '', + metadata: {}, + }); + }); + + describe('memoization', () => { + const initialState = { + entities: { + posts: { + posts: { + post1: {id: 'post1'}, + post2: {id: 'post2'}, + }, + }, + }, + }; + const initialCombinedId = 'user-activity-post1_post2'; + + test('should not recalculate when called twice with the same ID', () => { + const generateCombinedPost = makeGenerateCombinedPost(); + + expect(generateCombinedPost.recomputations()).toBe(0); + + generateCombinedPost(initialState, initialCombinedId); + + expect(generateCombinedPost.recomputations()).toBe(1); + + generateCombinedPost(initialState, initialCombinedId); + + expect(generateCombinedPost.recomputations()).toBe(1); + }); + + test('should recalculate when called twice with different IDs', () => { + const generateCombinedPost = makeGenerateCombinedPost(); + + expect(generateCombinedPost.recomputations()).toBe(0); + + let combinedId = initialCombinedId; + generateCombinedPost(initialState, combinedId); + + expect(generateCombinedPost.recomputations()).toBe(1); + + combinedId = 'user-activity-post2'; + generateCombinedPost(initialState, combinedId); + + expect(generateCombinedPost.recomputations()).toBe(2); + }); + + test('should not recalculate when a different post changes', () => { + const generateCombinedPost = makeGenerateCombinedPost(); + + expect(generateCombinedPost.recomputations()).toBe(0); + + let state = initialState; + generateCombinedPost(state, initialCombinedId); + + expect(generateCombinedPost.recomputations()).toBe(1); + + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + post3: {id: 'post3'}, + }, + }, + }, + }; + generateCombinedPost(state, initialCombinedId); + + expect(generateCombinedPost.recomputations()).toBe(1); + }); + + test('should recalculate when one of the included posts change', () => { + const generateCombinedPost = makeGenerateCombinedPost(); + + expect(generateCombinedPost.recomputations()).toBe(0); + + let state = initialState; + generateCombinedPost(state, initialCombinedId); + + expect(generateCombinedPost.recomputations()).toBe(1); + + state = { + ...state, + entities: { + ...state.entities, + posts: { + ...state.entities.posts, + posts: { + ...state.entities.posts.posts, + post2: {id: 'post2', update_at: 1234}, + }, + }, + }, + }; + generateCombinedPost(state, initialCombinedId); + + expect(generateCombinedPost.recomputations()).toBe(2); + }); + }); +}); + +describe('combineUserActivitySystemPost', () => { + const PostTypes = Posts.POST_TYPES; + + it('should return null', () => { + assert.equal(Boolean(combineUserActivitySystemPost()), false); + assert.equal(Boolean(combineUserActivitySystemPost([])), false); + }); + + const postAddToChannel1 = {type: PostTypes.ADD_TO_CHANNEL, user_id: 'user_id_1', props: {addedUserId: 'added_user_id_1', addedUsername: 'added_username_1'}}; + const postAddToChannel2 = {type: PostTypes.ADD_TO_CHANNEL, user_id: 'user_id_1', props: {addedUserId: 'added_user_id_2', addedUsername: 'added_username_2'}}; + const postAddToChannel3 = {type: PostTypes.ADD_TO_CHANNEL, user_id: 'user_id_1', props: {addedUserId: 'added_user_id_3', addedUsername: 'added_username_3'}}; + const postAddToChannel4 = {type: PostTypes.ADD_TO_CHANNEL, user_id: 'user_id_2', props: {addedUserId: 'added_user_id_4', addedUsername: 'added_username_4'}}; + const postAddToChannel5 = {type: PostTypes.ADD_TO_CHANNEL, user_id: 'user_id_1', props: {addedUsername: 'added_username_1'}}; + it('should match return for ADD_TO_CHANNEL', () => { + const out1 = { + allUserIds: ['added_user_id_1', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1]), out1); + + const out2 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1, postAddToChannel2]), out2); + + const out3 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1, postAddToChannel2, postAddToChannel3]), out3); + + const out4 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1', 'added_user_id_4', 'user_id_2'], + allUsernames: [], + messageData: [ + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1, postAddToChannel2, postAddToChannel3, postAddToChannel4]), out4); + + const out5 = { + allUserIds: ['user_id_1'], + allUsernames: ['added_username_1'], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_username_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel5]), out5); + + const out6 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1', 'added_user_id_4', 'user_id_2'], + allUsernames: ['added_username_1'], + messageData: [ + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_username_1', 'added_user_id_1', 'added_user_id_2', 'added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1, postAddToChannel2, postAddToChannel3, postAddToChannel4, postAddToChannel5]), out6); + }); + + it('should match return for ADD_TO_CHANNEL, backward compatibility with addedUsername', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: ['added_user_name_1'], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_name_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([{...postAddToChannel1, props: {addedUsername: 'added_user_name_1'}}]), out1); + + const out2 = { + allUserIds: ['added_user_id_2', 'user_id_1'], + allUsernames: ['added_user_name_1'], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_name_1', 'added_user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([{...postAddToChannel1, props: {addedUsername: 'added_user_name_1'}}, postAddToChannel2]), out2); + + const out3 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1', 'user_id_2'], + allUsernames: ['added_user_name_4'], + messageData: [ + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_name_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1, postAddToChannel2, postAddToChannel3, {...postAddToChannel4, props: {addedUsername: 'added_user_name_4'}}]), out3); + }); + + const postAddToTeam1 = {type: PostTypes.ADD_TO_TEAM, user_id: 'user_id_1', props: {addedUserId: 'added_user_id_1'}}; + const postAddToTeam2 = {type: PostTypes.ADD_TO_TEAM, user_id: 'user_id_1', props: {addedUserId: 'added_user_id_2'}}; + const postAddToTeam3 = {type: PostTypes.ADD_TO_TEAM, user_id: 'user_id_1', props: {addedUserId: 'added_user_id_3'}}; + const postAddToTeam4 = {type: PostTypes.ADD_TO_TEAM, user_id: 'user_id_2', props: {addedUserId: 'added_user_id_4'}}; + it('should match return for ADD_TO_TEAM', () => { + const out1 = { + allUserIds: ['added_user_id_1', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToTeam1]), out1); + + const out2 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_1', 'added_user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToTeam1, postAddToTeam2]), out2); + + const out3 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToTeam1, postAddToTeam2, postAddToTeam3]), out3); + + const out4 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1', 'added_user_id_4', 'user_id_2'], + allUsernames: [], + messageData: [ + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToTeam1, postAddToTeam2, postAddToTeam3, postAddToTeam4]), out4); + }); + + it('should match return for ADD_TO_TEAM, backward compatibility with addedUsername', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: ['added_user_name_1'], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_name_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([{...postAddToTeam1, props: {addedUsername: 'added_user_name_1'}}]), out1); + + const out2 = { + allUserIds: ['added_user_id_2', 'user_id_1'], + allUsernames: ['added_user_name_1'], + messageData: [{actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_name_1', 'added_user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([{...postAddToTeam1, props: {addedUsername: 'added_user_name_1'}}, postAddToTeam2]), out2); + + const out3 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3', 'user_id_1', 'user_id_2'], + allUsernames: ['added_user_name_4'], + messageData: [ + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_name_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToTeam1, postAddToTeam2, postAddToTeam3, {...postAddToTeam4, props: {addedUsername: 'added_user_name_4'}}]), out3); + }); + + const postJoinChannel1 = {type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_1'}; + const postJoinChannel2 = {type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_2'}; + const postJoinChannel3 = {type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_3'}; + const postJoinChannel4 = {type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_4'}; + it('should match return for JOIN_CHANNEL', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinChannel1]), out1); + + const out2 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinChannel1, postJoinChannel2]), out2); + + const out3 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinChannel1, postJoinChannel2, postJoinChannel3]), out3); + + const out4 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinChannel1, postJoinChannel2, postJoinChannel3, postJoinChannel4]), out4); + }); + + const postJoinTeam1 = {type: PostTypes.JOIN_TEAM, user_id: 'user_id_1'}; + const postJoinTeam2 = {type: PostTypes.JOIN_TEAM, user_id: 'user_id_2'}; + const postJoinTeam3 = {type: PostTypes.JOIN_TEAM, user_id: 'user_id_3'}; + const postJoinTeam4 = {type: PostTypes.JOIN_TEAM, user_id: 'user_id_4'}; + it('should match return for JOIN_TEAM', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_TEAM, userIds: ['user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinTeam1]), out1); + + const out2 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_TEAM, userIds: ['user_id_1', 'user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinTeam1, postJoinTeam2]), out2); + + const out3 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_TEAM, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinTeam1, postJoinTeam2, postJoinTeam3]), out3); + + const out4 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4'], + allUsernames: [], + messageData: [{postType: PostTypes.JOIN_TEAM, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinTeam1, postJoinTeam2, postJoinTeam3, postJoinTeam4]), out4); + }); + + const postLeaveChannel1 = {type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_1'}; + const postLeaveChannel2 = {type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_2'}; + const postLeaveChannel3 = {type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_3'}; + const postLeaveChannel4 = {type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_4'}; + it('should match return for LEAVE_CHANNEL', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveChannel1]), out1); + + const out2 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveChannel1, postLeaveChannel2]), out2); + + const out3 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveChannel1, postLeaveChannel2, postLeaveChannel3]), out3); + + const out4 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveChannel1, postLeaveChannel2, postLeaveChannel3, postLeaveChannel4]), out4); + }); + + const postLeaveTeam1 = {type: PostTypes.LEAVE_TEAM, user_id: 'user_id_1'}; + const postLeaveTeam2 = {type: PostTypes.LEAVE_TEAM, user_id: 'user_id_2'}; + const postLeaveTeam3 = {type: PostTypes.LEAVE_TEAM, user_id: 'user_id_3'}; + const postLeaveTeam4 = {type: PostTypes.LEAVE_TEAM, user_id: 'user_id_4'}; + it('should match return for LEAVE_TEAM', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveTeam1]), out1); + + const out2 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_1', 'user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveTeam1, postLeaveTeam2]), out2); + + const out3 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveTeam1, postLeaveTeam2, postLeaveTeam3]), out3); + + const out4 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4'], + allUsernames: [], + messageData: [{postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveTeam1, postLeaveTeam2, postLeaveTeam3, postLeaveTeam4]), out4); + }); + + const postRemoveFromChannel1 = {type: PostTypes.REMOVE_FROM_CHANNEL, user_id: 'user_id_1', props: {removedUserId: 'removed_user_id_1'}}; + const postRemoveFromChannel2 = {type: PostTypes.REMOVE_FROM_CHANNEL, user_id: 'user_id_1', props: {removedUserId: 'removed_user_id_2'}}; + const postRemoveFromChannel3 = {type: PostTypes.REMOVE_FROM_CHANNEL, user_id: 'user_id_1', props: {removedUserId: 'removed_user_id_3'}}; + const postRemoveFromChannel4 = {type: PostTypes.REMOVE_FROM_CHANNEL, user_id: 'user_id_1', props: {removedUserId: 'removed_user_id_4'}}; + it('should match return for REMOVE_FROM_CHANNEL', () => { + const out1 = { + allUserIds: ['removed_user_id_1', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromChannel1]), out1); + + const out2 = { + allUserIds: ['removed_user_id_1', 'removed_user_id_2', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1', 'removed_user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromChannel1, postRemoveFromChannel2]), out2); + + const out3 = { + allUserIds: ['removed_user_id_1', 'removed_user_id_2', 'removed_user_id_3', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1', 'removed_user_id_2', 'removed_user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromChannel1, postRemoveFromChannel2, postRemoveFromChannel3]), out3); + + const out4 = { + allUserIds: ['removed_user_id_1', 'removed_user_id_2', 'removed_user_id_3', 'removed_user_id_4', 'user_id_1'], + allUsernames: [], + messageData: [{actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1', 'removed_user_id_2', 'removed_user_id_3', 'removed_user_id_4']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromChannel1, postRemoveFromChannel2, postRemoveFromChannel3, postRemoveFromChannel4]), out4); + }); + + const postRemoveFromTeam1 = {type: PostTypes.REMOVE_FROM_TEAM, user_id: 'user_id_1'}; + const postRemoveFromTeam2 = {type: PostTypes.REMOVE_FROM_TEAM, user_id: 'user_id_2'}; + const postRemoveFromTeam3 = {type: PostTypes.REMOVE_FROM_TEAM, user_id: 'user_id_3'}; + const postRemoveFromTeam4 = {type: PostTypes.REMOVE_FROM_TEAM, user_id: 'user_id_4'}; + it('should match return for REMOVE_FROM_TEAM', () => { + const out1 = { + allUserIds: ['user_id_1'], + allUsernames: [], + messageData: [{postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_1']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromTeam1]), out1); + + const out2 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [{postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_1', 'user_id_2']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromTeam1, postRemoveFromTeam2]), out2); + + const out3 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [{postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromTeam1, postRemoveFromTeam2, postRemoveFromTeam3]), out3); + + const out4 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4'], + allUsernames: [], + messageData: [{postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromTeam1, postRemoveFromTeam2, postRemoveFromTeam3, postRemoveFromTeam4]), out4); + }); + + it('should match return on combination', () => { + const out1 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'user_id_1'], + allUsernames: [], + messageData: [ + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_1', 'added_user_id_2']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postAddToChannel1, postAddToChannel2, postAddToTeam1, postAddToTeam2]), out1); + + const out2 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_TEAM, userIds: ['user_id_1', 'user_id_2']}, + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postJoinChannel1, postJoinChannel2, postJoinTeam1, postJoinTeam2]), out2); + + const out3 = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_1', 'user_id_2']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postLeaveChannel1, postLeaveChannel2, postLeaveTeam1, postLeaveTeam2]), out3); + + const out4 = { + allUserIds: ['removed_user_id_1', 'removed_user_id_2', 'user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_1', 'user_id_2']}, + {actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1', 'removed_user_id_2']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([postRemoveFromChannel1, postRemoveFromChannel2, postRemoveFromTeam1, postRemoveFromTeam2]), out4); + + const out5 = { + allUserIds: ['added_user_id_1', 'added_user_id_2', 'user_id_1', 'user_id_2', 'removed_user_id_1', 'removed_user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2']}, + {actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1', 'removed_user_id_2']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([ + postAddToChannel1, + postJoinChannel1, + postLeaveChannel1, + postRemoveFromChannel1, + postAddToChannel2, + postJoinChannel2, + postLeaveChannel2, + postRemoveFromChannel2, + ]), out5); + + const out6 = { + allUserIds: ['added_user_id_3', 'user_id_1', 'added_user_id_4', 'user_id_2', 'user_id_3', 'user_id_4'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_TEAM, userIds: ['user_id_3', 'user_id_4']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_4']}, + {postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_3', 'user_id_4']}, + {postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_3', 'user_id_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([ + postAddToTeam3, + postJoinTeam3, + postLeaveTeam3, + postRemoveFromTeam3, + postAddToTeam4, + postJoinTeam4, + postLeaveTeam4, + postRemoveFromTeam4, + ]), out6); + + const out7 = { + allUserIds: ['added_user_id_3', 'added_user_id_1', 'added_user_id_2', 'user_id_1', 'added_user_id_4', 'user_id_2', 'user_id_3', 'user_id_4', 'removed_user_id_1', 'removed_user_id_2', 'removed_user_id_3', 'removed_user_id_4'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_TEAM, userIds: ['user_id_3', 'user_id_4', 'user_id_1', 'user_id_2']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_3', 'added_user_id_1', 'added_user_id_2']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_4']}, + {postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_3', 'user_id_4', 'user_id_1', 'user_id_2']}, + {postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_3', 'user_id_4', 'user_id_1', 'user_id_2']}, + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1', 'added_user_id_2', 'added_user_id_3']}, + {actorId: 'user_id_2', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_4']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4']}, + {actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1', 'removed_user_id_2', 'removed_user_id_3', 'removed_user_id_4']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([ + postAddToTeam3, + postJoinTeam3, + postLeaveTeam3, + postRemoveFromTeam3, + postAddToTeam4, + postJoinTeam4, + postLeaveTeam4, + postRemoveFromTeam4, + + postAddToChannel1, + postJoinChannel1, + postLeaveChannel1, + postRemoveFromChannel1, + postAddToChannel2, + postJoinChannel2, + postLeaveChannel2, + postRemoveFromChannel2, + + postAddToChannel3, + postJoinChannel3, + postLeaveChannel3, + postRemoveFromChannel3, + postAddToChannel4, + postJoinChannel4, + postLeaveChannel4, + postRemoveFromChannel4, + + postAddToTeam1, + postJoinTeam1, + postLeaveTeam1, + postRemoveFromTeam1, + postAddToTeam2, + postJoinTeam2, + postLeaveTeam2, + postRemoveFromTeam2, + ]), out7); + + const out8 = { + allUserIds: ['added_user_id_3', 'user_id_1', 'user_id_3', 'added_user_id_1', 'removed_user_id_1'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_TEAM, userIds: ['user_id_3']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_TEAM, userIds: ['added_user_id_3']}, + {postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_3']}, + {postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['user_id_3']}, + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {actorId: 'user_id_1', postType: PostTypes.ADD_TO_CHANNEL, userIds: ['added_user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + {actorId: 'user_id_1', postType: PostTypes.REMOVE_FROM_CHANNEL, userIds: ['removed_user_id_1']}, + ], + }; + assert.deepEqual(combineUserActivitySystemPost([ + postAddToTeam3, + postAddToTeam3, + postJoinTeam3, + postJoinTeam3, + postLeaveTeam3, + postLeaveTeam3, + postRemoveFromTeam3, + postRemoveFromTeam3, + + postAddToChannel1, + postAddToChannel1, + postJoinChannel1, + postJoinChannel1, + postLeaveChannel1, + postLeaveChannel1, + postRemoveFromChannel1, + postRemoveFromChannel1, + ]), out8); + }); +}); + +describe('comparePostTypes', () => { + const { + JOIN_TEAM, + ADD_TO_TEAM, + LEAVE_TEAM, + REMOVE_FROM_TEAM, + JOIN_CHANNEL, + ADD_TO_CHANNEL, + LEAVE_CHANNEL, + REMOVE_FROM_CHANNEL, + } = Posts.POST_TYPES; + + const testCases = [ + [], + [{postType: JOIN_TEAM}], + [{postType: JOIN_TEAM}, {postType: ADD_TO_TEAM}], + [{postType: ADD_TO_TEAM}, {postType: JOIN_TEAM}], + [{postType: ADD_TO_TEAM}, {postType: ADD_TO_TEAM}, {postType: JOIN_TEAM}], + [{postType: JOIN_TEAM}, {postType: ADD_TO_TEAM}, {postType: LEAVE_TEAM}, {postType: REMOVE_FROM_TEAM}], + [{postType: REMOVE_FROM_TEAM}, {postType: LEAVE_TEAM}, {postType: ADD_TO_TEAM}, {postType: JOIN_TEAM}], + [{postType: JOIN_CHANNEL}, {postType: ADD_TO_CHANNEL}, {postType: LEAVE_CHANNEL}, {postType: REMOVE_FROM_CHANNEL}], + [{postType: REMOVE_FROM_CHANNEL}, {postType: LEAVE_CHANNEL}, {postType: ADD_TO_CHANNEL}, {postType: JOIN_CHANNEL}], + [{postType: LEAVE_CHANNEL}, {postType: REMOVE_FROM_CHANNEL}, {postType: LEAVE_TEAM}, {postType: REMOVE_FROM_TEAM}], + [{postType: LEAVE_TEAM}, {postType: REMOVE_FROM_TEAM}, {postType: LEAVE_CHANNEL}, {postType: REMOVE_FROM_CHANNEL}], + [{postType: JOIN_CHANNEL}, {postType: LEAVE_CHANNEL}, {postType: JOIN_CHANNEL}, {postType: REMOVE_FROM_CHANNEL}, {postType: ADD_TO_CHANNEL}], + ]; + + it('should sort post type correctly', () => { + for (const testCase of testCases) { + let previousType; + testCase.sort(comparePostTypes).forEach((sortedTestCase, index) => { + if (index > 0) { + assert.ok(postTypePriority[previousType] <= postTypePriority[sortedTestCase.postType], `${previousType} should come first before ${sortedTestCase.postType}`); + } + + previousType = sortedTestCase.postType; + }); + } + }); +}); diff --git a/app/mm-redux/utils/post_list.ts b/app/mm-redux/utils/post_list.ts new file mode 100644 index 000000000..2fc4822b9 --- /dev/null +++ b/app/mm-redux/utils/post_list.ts @@ -0,0 +1,435 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import * as reselect from 'reselect'; +import moment from 'moment-timezone'; +import {Posts, Preferences} from '../constants'; +import {makeGetPostsForIds} from '@mm-redux/selectors/entities/posts'; +import {getBool} from '@mm-redux/selectors/entities/preferences'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {createIdsSelector, memoizeResult} from '@mm-redux/utils/helpers'; +import {isUserActivityPost, shouldFilterJoinLeavePost} from '@mm-redux/utils/post_utils'; +import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; +import * as types from '@mm-redux/types'; +export const COMBINED_USER_ACTIVITY = 'user-activity-'; +export const DATE_LINE = 'date-'; +export const START_OF_NEW_MESSAGES = 'start-of-new-messages'; +export const MAX_COMBINED_SYSTEM_POSTS = 100; + +import {GlobalState} from '@mm-redux/types/store'; + +function shouldShowJoinLeaveMessages(state: GlobalState) { + // This setting is true or not set if join/leave messages are to be displayed + return getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true); +} + +interface PostFilterOptions { + postIds: string[]; + lastViewedAt: number; + indicateNewMessages: boolean; +} + +export function makePreparePostIdsForPostList() { + const filterPostsAndAddSeparators = makeFilterPostsAndAddSeparators(); + const combineUserActivityPosts = makeCombineUserActivityPosts(); + return (state: GlobalState, options: PostFilterOptions) => { + let postIds = filterPostsAndAddSeparators(state, options); + postIds = combineUserActivityPosts(state, postIds); + return postIds; + }; +} + +// Returns a selector that, given the state and an object containing an array of postIds and an optional +// timestamp of when the channel was last read, returns a memoized array of postIds interspersed with +// day indicators and an optional new message indicator. +export function makeFilterPostsAndAddSeparators() { + const getPostsForIds = makeGetPostsForIds(); + + return createIdsSelector( + (state: GlobalState, {postIds}: PostFilterOptions) => getPostsForIds(state, postIds), + (state: GlobalState, {lastViewedAt}: PostFilterOptions) => lastViewedAt, + (state: GlobalState, {indicateNewMessages}: PostFilterOptions) => indicateNewMessages, + (state) => state.entities.posts.selectedPostId, + getCurrentUser, + shouldShowJoinLeaveMessages, + isTimezoneEnabled, + (posts, lastViewedAt, indicateNewMessages, selectedPostId, currentUser, showJoinLeave, timeZoneEnabled) => { + if (posts.length === 0 || !currentUser) { + return []; + } + + const out: string[] = []; + let lastDate; + let addedNewMessagesIndicator = false; + + // Iterating through the posts from oldest to newest + for (let i = posts.length - 1; i >= 0; i--) { + const post = posts[i]; + + if ( + !post || + (post.type === Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL && !selectedPostId) + ) { + continue; + } + + // Filter out join/leave messages if necessary + if (shouldFilterJoinLeavePost(post, showJoinLeave, currentUser.username)) { + continue; + } + + // Push on a date header if the last post was on a different day than the current one + const postDate = new Date(post.create_at); + if (timeZoneEnabled) { + const currentOffset = postDate.getTimezoneOffset() * 60 * 1000; + const timezone = getUserCurrentTimezone(currentUser.timezone); + if (timezone) { + const zone = moment.tz.zone(timezone); + if (zone) { + const timezoneOffset = zone.utcOffset(post.create_at) * 60 * 1000; + postDate.setTime(post.create_at + (currentOffset - timezoneOffset)); + } + } + } + + if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) { + out.push(DATE_LINE + postDate.getTime()); + + lastDate = postDate; + } + + if ( + lastViewedAt && + post.create_at > lastViewedAt && + post.user_id !== currentUser.id && + !addedNewMessagesIndicator && + indicateNewMessages + ) { + out.push(START_OF_NEW_MESSAGES); + addedNewMessagesIndicator = true; + } + + out.push(post.id); + } + + // Flip it back to newest to oldest + return out.reverse(); + } + ); +} + +export function makeCombineUserActivityPosts() { + return createIdsSelector( + (state: GlobalState, postIds: string[]) => postIds, + (state) => state.entities.posts.posts, + (postIds, posts) => { + let lastPostIsUserActivity = false; + let combinedCount = 0; + const out: string[] = []; + let changed = false; + + for (let i = 0; i < postIds.length; i++) { + const postId = postIds[i]; + + if (postId === START_OF_NEW_MESSAGES || postId.startsWith(DATE_LINE)) { + // Not a post, so it won't be combined + out.push(postId); + + lastPostIsUserActivity = false; + combinedCount = 0; + + continue; + } + + const post = posts[postId]; + const postIsUserActivity = isUserActivityPost(post.type); + + if (postIsUserActivity && lastPostIsUserActivity && combinedCount < MAX_COMBINED_SYSTEM_POSTS) { + // Add the ID to the previous combined post + out[out.length - 1] += '_' + postId; + + combinedCount += 1; + + changed = true; + } else if (postIsUserActivity) { + // Start a new combined post, even if the "combined" post is only a single post + out.push(COMBINED_USER_ACTIVITY + postId); + + combinedCount = 1; + + changed = true; + } else { + out.push(postId); + + combinedCount = 0; + } + + lastPostIsUserActivity = postIsUserActivity; + } + + if (!changed) { + // Nothing was combined, so return the original array + return postIds; + } + + return out; + }, + ); +} + +export function isStartOfNewMessages(item: string) { + return item === START_OF_NEW_MESSAGES; +} + +export function isDateLine(item: string) { + return item.startsWith(DATE_LINE); +} + +export function getDateForDateLine(item: string) { + return parseInt(item.substring(DATE_LINE.length), 10); +} + +export function isCombinedUserActivityPost(item: string) { + return (/^user-activity-(?:[^_]+_)*[^_]+$/).test(item); +} + +export function getPostIdsForCombinedUserActivityPost(item: string) { + return item.substring(COMBINED_USER_ACTIVITY.length).split('_'); +} + +export function getFirstPostId(items: string[]) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + if (isStartOfNewMessages(item) || isDateLine(item)) { + // This is not a post at all + continue; + } + + if (isCombinedUserActivityPost(item)) { + // This is a combined post, so find the first post ID from it + const combinedIds = getPostIdsForCombinedUserActivityPost(item); + + return combinedIds[0]; + } + + // This is a post ID + return item; + } + + return ''; +} + +export function getLastPostId(items: string[]) { + for (let i = items.length - 1; i >= 0; i--) { + const item = items[i]; + + if (isStartOfNewMessages(item) || isDateLine(item)) { + // This is not a post at all + continue; + } + + if (isCombinedUserActivityPost(item)) { + // This is a combined post, so find the first post ID from it + const combinedIds = getPostIdsForCombinedUserActivityPost(item); + + return combinedIds[combinedIds.length - 1]; + } + + // This is a post ID + return item; + } + + return ''; +} + +export function getLastPostIndex(postIds: string[]) { + let index = 0; + for (let i = postIds.length - 1; i > 0; i--) { + const item = postIds[i]; + if (!isStartOfNewMessages(item) && !isDateLine(item)) { + index = i; + break; + } + } + + return index; +} + +export function makeGenerateCombinedPost() { + const getPostsForIds = makeGetPostsForIds(); + const getPostIds = memoizeResult(getPostIdsForCombinedUserActivityPost); + + return reselect.createSelector( + (state: GlobalState, combinedId: string) => combinedId, + (state: GlobalState, combinedId: string) => getPostsForIds(state, getPostIds(combinedId)), + (combinedId, posts) => { + // All posts should be in the same channel + const channelId = posts[0].channel_id; + + // Assume that the last post is the oldest one + const createAt = posts[posts.length - 1].create_at; + + const messages = posts.map((post) => post.message); + + return { + id: combinedId, + root_id: '', + channel_id: channelId, + create_at: createAt, + delete_at: 0, + message: messages.join('\n'), + props: { + messages, + user_activity: combineUserActivitySystemPost(posts), + }, + state: '', + system_post_ids: posts.map((post) => post.id), + type: Posts.POST_TYPES.COMBINED_USER_ACTIVITY, + user_activity_posts: posts, + user_id: '', + metadata: {}, + }; + } + ); +} + +export const postTypePriority = { + [Posts.POST_TYPES.JOIN_TEAM]: 0, + [Posts.POST_TYPES.ADD_TO_TEAM]: 1, + [Posts.POST_TYPES.LEAVE_TEAM]: 2, + [Posts.POST_TYPES.REMOVE_FROM_TEAM]: 3, + [Posts.POST_TYPES.JOIN_CHANNEL]: 4, + [Posts.POST_TYPES.ADD_TO_CHANNEL]: 5, + [Posts.POST_TYPES.LEAVE_CHANNEL]: 6, + [Posts.POST_TYPES.REMOVE_FROM_CHANNEL]: 7, + [Posts.POST_TYPES.PURPOSE_CHANGE]: 8, + [Posts.POST_TYPES.HEADER_CHANGE]: 9, + [Posts.POST_TYPES.JOIN_LEAVE]: 10, + [Posts.POST_TYPES.DISPLAYNAME_CHANGE]: 11, + [Posts.POST_TYPES.CONVERT_CHANNEL]: 12, + [Posts.POST_TYPES.CHANNEL_DELETED]: 13, + [Posts.POST_TYPES.CHANNEL_UNARCHIVED]: 14, + [Posts.POST_TYPES.ADD_REMOVE]: 15, + [Posts.POST_TYPES.EPHEMERAL]: 16, +}; + +export function comparePostTypes(a: typeof postTypePriority, b: typeof postTypePriority) { + return postTypePriority[a.postType] - postTypePriority[b.postType]; +} + +function extractUserActivityData(userActivities: any) { + const messageData: any[] = []; + const allUserIds: string[] = []; + const allUsernames: string[] = []; + Object.entries(userActivities).forEach(([postType, values]: [string, any]) => { + if ( + postType === Posts.POST_TYPES.ADD_TO_TEAM || + postType === Posts.POST_TYPES.ADD_TO_CHANNEL || + postType === Posts.POST_TYPES.REMOVE_FROM_CHANNEL + ) { + Object.keys(values).map((key) => [key, values[key]]).forEach(([actorId, users]) => { + if (Array.isArray(users)) { + throw new Error('Invalid Post activity data'); + } + const {ids, usernames} = users; + messageData.push({postType, userIds: [...usernames, ...ids], actorId}); + if (ids.length > 0) { + allUserIds.push(...ids); + } + + if (usernames.length > 0) { + allUsernames.push(...usernames); + } + allUserIds.push(actorId); + }); + } else { + if (!Array.isArray(values)) { + throw new Error('Invalid Post activity data'); + } + messageData.push({postType, userIds: values}); + allUserIds.push(...values); + } + }); + + messageData.sort(comparePostTypes); + + function reduceUsers(acc: string[], curr: string) { + if (!acc.includes(curr)) { + acc.push(curr); + } + return acc; + } + + return { + allUserIds: allUserIds.reduce(reduceUsers, []), + allUsernames: allUsernames.reduce(reduceUsers, []), + messageData, + }; +} + +export function combineUserActivitySystemPost(systemPosts: Array = []) { + if (systemPosts.length === 0) { + return null; + } + + const userActivities = systemPosts.reduce((acc: any, post: types.posts.Post) => { + const postType = post.type; + let userActivityProps = acc; + const combinedPostType = userActivityProps[postType as string]; + + if ( + postType === Posts.POST_TYPES.ADD_TO_TEAM || + postType === Posts.POST_TYPES.ADD_TO_CHANNEL || + postType === Posts.POST_TYPES.REMOVE_FROM_CHANNEL + ) { + const userId = post.props.addedUserId || post.props.removedUserId; + const username = post.props.addedUsername || post.props.removedUsername; + if (combinedPostType) { + if (Array.isArray(combinedPostType[post.user_id])) { + throw new Error('Invalid Post activity data'); + } + const users = combinedPostType[post.user_id] || {ids: [], usernames: []}; + if (userId) { + if (!users.ids.includes(userId)) { + users.ids.push(userId); + } + } else if (username && !users.usernames.includes(username)) { + users.usernames.push(username); + } + combinedPostType[post.user_id] = users; + } else { + const users = { + ids: [] as string[], + usernames: [] as string[], + }; + + if (userId) { + users.ids.push(userId); + } else if (username) { + users.usernames.push(username); + } + userActivityProps[postType] = { + [post.user_id]: users, + }; + } + } else { + const propsUserId = post.user_id; + + if (combinedPostType) { + if (!Array.isArray(combinedPostType)) { + throw new Error('Invalid Post activity data'); + } + if (!combinedPostType.includes(propsUserId)) { + userActivityProps[postType] = [...combinedPostType, propsUserId]; + } + } else { + userActivityProps = {...userActivityProps, [postType]: [propsUserId]}; + } + } + + return userActivityProps; + }, {}); + + return extractUserActivityData(userActivities); +} diff --git a/app/mm-redux/utils/post_utils.test.js b/app/mm-redux/utils/post_utils.test.js new file mode 100644 index 000000000..55cfef27e --- /dev/null +++ b/app/mm-redux/utils/post_utils.test.js @@ -0,0 +1,535 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {PostTypes} from '@mm-redux/constants/posts'; +import {Permissions} from '../constants'; + +import { + canEditPost, + isSystemMessage, + isMeMessage, + isUserActivityPost, + shouldFilterJoinLeavePost, + isPostCommentMention, + getEmbedFromMetadata, +} from '@mm-redux/utils/post_utils'; + +describe('PostUtils', () => { + describe('shouldFilterJoinLeavePost', () => { + it('show join/leave posts', () => { + const showJoinLeave = true; + + assert.equal(shouldFilterJoinLeavePost({type: ''}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.CHANNEL_DELETED}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.DISPLAYNAME_CHANGE}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.CONVERT_CHANNEL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.EPHEMERAL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.HEADER_CHANGE}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.PURPOSE_CHANGE}, showJoinLeave), false); + + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_LEAVE}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_CHANNEL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_CHANNEL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_REMOVE}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_CHANNEL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_CHANNEL}, showJoinLeave), false); + + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_TEAM}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_TEAM}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_TEAM}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_TEAM}, showJoinLeave), false); + }); + + it('hide join/leave posts', () => { + const showJoinLeave = false; + + assert.equal(shouldFilterJoinLeavePost({type: ''}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.CHANNEL_DELETED}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.DISPLAYNAME_CHANGE}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.CONVERT_CHANNEL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.EPHEMERAL}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.HEADER_CHANGE}, showJoinLeave), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.PURPOSE_CHANGE}, showJoinLeave), false); + + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_LEAVE}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_CHANNEL}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_CHANNEL}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_REMOVE}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_CHANNEL}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_CHANNEL}, showJoinLeave), true); + + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_TEAM}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_TEAM}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_TEAM}, showJoinLeave), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_TEAM}, showJoinLeave), true); + }); + + it('always join/leave posts for the current user', () => { + const username = 'user1'; + const otherUsername = 'user2'; + const showJoinLeave = false; + + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_CHANNEL, props: {username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_CHANNEL, props: {username: otherUsername}}, showJoinLeave, username), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_CHANNEL, props: {username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_CHANNEL, props: {username: otherUsername}}, showJoinLeave, username), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_CHANNEL, props: {username, addedUsername: otherUsername}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_CHANNEL, props: {username: otherUsername, addedUsername: username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_CHANNEL, props: {username: otherUsername, addedUsername: otherUsername}}, showJoinLeave, username), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_CHANNEL, props: {removedUsername: username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_CHANNEL, props: {removedUsername: otherUsername}}, showJoinLeave, username), true); + + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_TEAM, props: {username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.JOIN_TEAM, props: {username: otherUsername}}, showJoinLeave, username), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_TEAM, props: {username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.LEAVE_TEAM, props: {username: otherUsername}}, showJoinLeave, username), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_TEAM, props: {username, addedUsername: otherUsername}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_TEAM, props: {username: otherUsername, addedUsername: username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.ADD_TO_TEAM, props: {username: otherUsername, addedUsername: otherUsername}}, showJoinLeave, username), true); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_TEAM, props: {removedUsername: username}}, showJoinLeave, username), false); + assert.equal(shouldFilterJoinLeavePost({type: PostTypes.REMOVE_FROM_TEAM, props: {removedUsername: otherUsername}}, showJoinLeave, username), true); + }); + }); + + describe('canEditPost', () => { + const notLicensed = {IsLicensed: 'false'}; + const licensed = {IsLicensed: 'true'}; + const teamId = 'team-id'; + const channelId = 'channel-id'; + const userId = 'user-id'; + + const state = {entities: {general: {serverVersion: ''}}}; + + it('should allow to edit my post without license', () => { + // Hasn't license + assert.ok(canEditPost(state, {PostEditTimeLimit: -1}, notLicensed, teamId, channelId, userId, {user_id: userId, type: 'normal'})); + assert.ok(!canEditPost(state, {PostEditTimeLimit: -1}, notLicensed, teamId, channelId, userId, {user_id: userId, type: 'system_test'})); + assert.ok(!canEditPost(state, {PostEditTimeLimit: -1}, notLicensed, teamId, channelId, userId, {user_id: 'other', type: 'normal'})); + assert.ok(!canEditPost(state, {PostEditTimeLimit: -1}, notLicensed, teamId, channelId, userId, {user_id: 'other', type: 'system_test'})); + assert.ok(!canEditPost(state, {PostEditTimeLimit: -1}, notLicensed, teamId, channelId, userId, null)); + }); + + it('should work with old permissions version', () => { + const oldVersionState = { + entities: { + general: { + serverVersion: '4.3.0', + }, + }, + }; + + // With old permissions + assert.ok(!canEditPost(oldVersionState, {PostEditTimeLimit: null, AllowEditPost: 'never'}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(oldVersionState, {PostEditTimeLimit: null, AllowEditPost: 'always'}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(oldVersionState, {PostEditTimeLimit: 300, AllowEditPost: 'time_limit'}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(oldVersionState, {PostEditTimeLimit: 300, AllowEditPost: 'time_limit'}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 600000})); + assert.ok(!canEditPost(oldVersionState, {PostEditTimeLimit: null, AllowEditPost: 'never'}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(oldVersionState, {PostEditTimeLimit: null, AllowEditPost: 'always'}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(oldVersionState, {PostEditTimeLimit: 300, AllowEditPost: 'time_limit'}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(oldVersionState, {PostEditTimeLimit: 300, AllowEditPost: 'time_limit'}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 600000})); + }); + + it('should work with new permissions version', () => { + const newVersionState = { + entities: { + general: { + serverVersion: '4.9.0', + }, + users: { + currentUserId: userId, + profiles: { + 'user-id': {roles: 'system_role'}, + }, + }, + teams: { + currentTeamId: teamId, + myMembers: { + 'team-id': {roles: 'team_role'}, + }, + }, + channels: { + currentChannelId: channelId, + myMembers: { + 'channel-id': {roles: 'channel_role'}, + }, + }, + roles: { + roles: { + system_role: { + permissions: [], + }, + team_role: { + permissions: [], + }, + channel_role: { + permissions: [], + }, + }, + }, + }, + }; + + // With new permissions + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: [Permissions.EDIT_POST]}, + team_role: {permissions: []}, + channel_role: {permissions: []}, + }, + }; + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: []}, + team_role: {permissions: [Permissions.EDIT_POST]}, + channel_role: {permissions: []}, + }, + }; + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: []}, + team_role: {permissions: []}, + channel_role: {permissions: [Permissions.EDIT_POST]}, + }, + }; + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: [Permissions.EDIT_OTHERS_POSTS]}, + team_role: {permissions: []}, + channel_role: {permissions: []}, + }, + }; + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: []}, + team_role: {permissions: [Permissions.EDIT_OTHERS_POSTS]}, + channel_role: {permissions: []}, + }, + }; + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: []}, + team_role: {permissions: []}, + channel_role: {permissions: [Permissions.EDIT_OTHERS_POSTS]}, + }, + }; + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: [Permissions.EDIT_OTHERS_POSTS, Permissions.EDIT_POST]}, + team_role: {permissions: []}, + channel_role: {permissions: []}, + }, + }; + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: []}, + team_role: {permissions: [Permissions.EDIT_OTHERS_POSTS, Permissions.EDIT_POST]}, + channel_role: {permissions: []}, + }, + }; + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + + newVersionState.entities.roles = { + roles: { + system_role: {permissions: []}, + team_role: {permissions: []}, + channel_role: {permissions: [Permissions.EDIT_OTHERS_POSTS, Permissions.EDIT_POST]}, + }, + }; + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: userId})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: userId, create_at: Date.now() - 6000000})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: -1}, licensed, teamId, channelId, userId, {user_id: 'other'})); + assert.ok(canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 100})); + assert.ok(!canEditPost(newVersionState, {PostEditTimeLimit: 300}, licensed, teamId, channelId, userId, {user_id: 'other', create_at: Date.now() - 6000000})); + }); + }); + + describe('isSystemMessage', () => { + it('should identify if post is system message', () => { + const testCases = [ + {input: {type: ''}, output: false}, + + {input: {type: PostTypes.CHANNEL_DELETED}, output: true}, + {input: {type: PostTypes.CHANNEL_UNARCHIVED}, output: true}, + {input: {type: PostTypes.DISPLAYNAME_CHANGE}, output: true}, + {input: {type: PostTypes.CONVERT_CHANNEL}, output: true}, + {input: {type: PostTypes.EPHEMERAL}, output: true}, + {input: {type: PostTypes.EPHEMERAL_ADD_TO_CHANNEL}, output: true}, + {input: {type: PostTypes.HEADER_CHANGE}, output: true}, + {input: {type: PostTypes.PURPOSE_CHANGE}, output: true}, + + {input: {type: PostTypes.JOIN_LEAVE}, output: true}, // deprecated system type + {input: {type: PostTypes.ADD_REMOVE}, output: true}, // deprecated system type + + {input: {type: PostTypes.COMBINED_USER_ACTIVITY}, output: true}, + + {input: {type: PostTypes.ADD_TO_CHANNEL}, output: true}, + {input: {type: PostTypes.JOIN_CHANNEL}, output: true}, + {input: {type: PostTypes.LEAVE_CHANNEL}, output: true}, + {input: {type: PostTypes.REMOVE_FROM_CHANNEL}, output: true}, + {input: {type: PostTypes.ADD_TO_TEAM}, output: true}, + {input: {type: PostTypes.JOIN_TEAM}, output: true}, + {input: {type: PostTypes.LEAVE_TEAM}, output: true}, + {input: {type: PostTypes.REMOVE_FROM_TEAM}, output: true}, + ]; + + testCases.forEach((testCase) => { + assert.equal( + isSystemMessage(testCase.input), + testCase.output, + `isSystemMessage('${testCase.input}') should return ${testCase.output}`, + ); + }); + }); + }); + + describe('isUserActivityPost', () => { + it('should identify if post is user activity - add/remove/join/leave channel/team', () => { + const testCases = [ + {input: '', output: false}, + {input: null, output: false}, + + {input: PostTypes.CHANNEL_DELETED, output: false}, + {input: PostTypes.DISPLAYNAME_CHANGE, output: false}, + {input: PostTypes.CONVERT_CHANNEL, output: false}, + {input: PostTypes.EPHEMERAL, output: false}, + {input: PostTypes.EPHEMERAL_ADD_TO_CHANNEL, output: false}, + {input: PostTypes.HEADER_CHANGE, output: false}, + {input: PostTypes.PURPOSE_CHANGE, output: false}, + + {input: PostTypes.JOIN_LEAVE, output: false}, // deprecated system type + {input: PostTypes.ADD_REMOVE, output: false}, // deprecated system type + + {input: PostTypes.COMBINED_USER_ACTIVITY, output: false}, + + {input: PostTypes.ADD_TO_CHANNEL, output: true}, + {input: PostTypes.JOIN_CHANNEL, output: true}, + {input: PostTypes.LEAVE_CHANNEL, output: true}, + {input: PostTypes.REMOVE_FROM_CHANNEL, output: true}, + {input: PostTypes.ADD_TO_TEAM, output: true}, + {input: PostTypes.JOIN_TEAM, output: true}, + {input: PostTypes.LEAVE_TEAM, output: true}, + {input: PostTypes.REMOVE_FROM_TEAM, output: true}, + ]; + + testCases.forEach((testCase) => { + assert.equal( + isUserActivityPost(testCase.input), + testCase.output, + `isUserActivityPost('${testCase.input}') should return ${testCase.output}`, + ); + }); + }); + }); + + describe('isPostCommentMention', () => { + const currentUser = { + id: 'currentUser', + notify_props: { + comments: 'any', + }, + }; + it('should return true as root post is by user', () => { + const post = { + user_id: 'someotherUser', + }; + + const rootPost = { + user_id: 'currentUser', + }; + + const isCommentMention = isPostCommentMention({currentUser, post, rootPost, threadRepliedToByCurrentUser: false}); + assert.equal(isCommentMention, true); + }); + + it('should return false as root post is not by user and did not participate in thread', () => { + const post = { + user_id: 'someotherUser', + }; + + const rootPost = { + user_id: 'differentUser', + }; + + const isCommentMention = isPostCommentMention({currentUser, post, rootPost, threadRepliedToByCurrentUser: false}); + assert.equal(isCommentMention, false); + }); + + it('should return false post is by current User', () => { + const post = { + user_id: 'currentUser', + }; + + const rootPost = { + user_id: 'differentUser', + }; + + const isCommentMention = isPostCommentMention({currentUser, post, rootPost, threadRepliedToByCurrentUser: false}); + assert.equal(isCommentMention, false); + }); + + it('should return true as post is by current User but it is a webhhok and user participated in thread', () => { + const post = { + user_id: 'currentUser', + props: { + from_webhook: true, + }, + }; + + const rootPost = { + user_id: 'differentUser', + }; + + const isCommentMention = isPostCommentMention({currentUser, post, rootPost, threadRepliedToByCurrentUser: true}); + assert.equal(isCommentMention, true); + }); + + it('should return false as root post is not by currentUser and notify_props is root', () => { + const post = { + user_id: 'someotherUser', + }; + + const rootPost = { + user_id: 'differentUser', + }; + + const modifiedCurrentUser = { + ...currentUser, + notify_props: { + comments: 'root', + }, + }; + + const isCommentMention = isPostCommentMention({currentUser: modifiedCurrentUser, post, rootPost, threadRepliedToByCurrentUser: true}); + assert.equal(isCommentMention, false); + }); + + it('should return true as root post is by currentUser and notify_props is root', () => { + const post = { + user_id: 'someotherUser', + }; + + const rootPost = { + user_id: 'currentUser', + }; + + const modifiedCurrentUser = { + ...currentUser, + notify_props: { + comments: 'root', + }, + }; + + const isCommentMention = isPostCommentMention({currentUser: modifiedCurrentUser, post, rootPost, threadRepliedToByCurrentUser: true}); + assert.equal(isCommentMention, true); + }); + }); + + describe('isMeMessage', () => { + it('should correctly identify messages generated from /me', () => { + for (const data of [ + { + post: {type: 'hello'}, + result: false, + }, + { + post: {type: 'ME'}, + result: false, + }, + { + post: {type: PostTypes.ME}, + result: true, + }, + ]) { + const confirmation = isMeMessage(data.post); + assert.equal(confirmation, data.result, data.post); + } + }); + }); + + describe('getEmbedFromMetadata', () => { + it('should return null if no metadata is not passed as argument', () => { + const embedData = getEmbedFromMetadata(); + assert.equal(embedData, null); + }); + + it('should return null if argument does not contain embed key', () => { + const embedData = getEmbedFromMetadata({}); + assert.equal(embedData, null); + }); + + it('should return null if embed key in argument is empty', () => { + const embedData = getEmbedFromMetadata({embeds: []}); + assert.equal(embedData, null); + }); + + it('should return first entry in embed key', () => { + const embedValue = {type: 'opengraph', url: 'url'}; + const embedData = getEmbedFromMetadata({embeds: [embedValue, {type: 'image', url: 'url1'}]}); + assert.equal(embedData, embedValue); + }); + }); +}); diff --git a/app/mm-redux/utils/post_utils.ts b/app/mm-redux/utils/post_utils.ts new file mode 100644 index 000000000..2fc98c949 --- /dev/null +++ b/app/mm-redux/utils/post_utils.ts @@ -0,0 +1,227 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {General, Posts, Preferences, Permissions} from '../constants'; +import {hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles'; + +import {GlobalState} from '@mm-redux/types/store'; +import {PreferenceType} from '@mm-redux/types/preferences'; +import {Post, PostType, PostMetadata, PostEmbed} from '@mm-redux/types/posts'; +import {UserProfile} from '@mm-redux/types/users'; +import {Team} from '@mm-redux/types/teams'; +import {Channel} from '@mm-redux/types/channels'; +import {$ID} from '@mm-redux/types/utilities'; + +import {getPreferenceKey} from './preference_utils'; + +export function isPostFlagged(postId: $ID, myPreferences: { + [x: string]: PreferenceType; +}): boolean { + const key = getPreferenceKey(Preferences.CATEGORY_FLAGGED_POST, postId); + return myPreferences.hasOwnProperty(key); +} + +export function isSystemMessage(post: Post): boolean { + return Boolean(post.type && post.type.startsWith(Posts.SYSTEM_MESSAGE_PREFIX)); +} + +export function isMeMessage(post: Post): boolean { + return Boolean(post.type && post.type === Posts.POST_TYPES.ME); +} + +export function isFromWebhook(post: Post): boolean { + return post.props && post.props.from_webhook; +} + +export function isPostEphemeral(post: Post): boolean { + return post.type === Posts.POST_TYPES.EPHEMERAL || post.type === Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL || post.state === Posts.POST_DELETED; +} + +export function shouldIgnorePost(post: Post): boolean { + return Posts.IGNORE_POST_TYPES.includes(post.type); +} + +export function isUserActivityPost(postType: PostType): boolean { + return Posts.USER_ACTIVITY_POST_TYPES.includes(postType); +} + +export function isPostOwner(userId: $ID, post: Post) { + return userId === post.user_id; +} + +export function isEdited(post: Post): boolean { + return post.edit_at > 0; +} + +export function canDeletePost(state: GlobalState, config: any, license: any, teamId: $ID, channelId: $ID, userId: $ID, post: Post, isAdmin: boolean, isSystemAdmin: boolean): boolean { + if (!post) { + return false; + } + + const isOwner = isPostOwner(userId, post); + + if (hasNewPermissions(state)) { + const canDelete = haveIChannelPermission(state, {team: teamId, channel: channelId, permission: Permissions.DELETE_POST}); + if (!isOwner) { + return canDelete && haveIChannelPermission(state, {team: teamId, channel: channelId, permission: Permissions.DELETE_OTHERS_POSTS}); + } + return canDelete; + } + + // Backwards compatibility with pre-advanced permissions config settings. + if (license.IsLicensed === 'true') { + return (config.RestrictPostDelete === General.PERMISSIONS_ALL && (isOwner || isAdmin)) || + (config.RestrictPostDelete === General.PERMISSIONS_TEAM_ADMIN && isAdmin) || + (config.RestrictPostDelete === General.PERMISSIONS_SYSTEM_ADMIN && isSystemAdmin); + } + return isOwner || isAdmin; +} + +export function canEditPost(state: GlobalState, config: any, license: any, teamId: $ID, channelId: $ID, userId: $ID, post: Post): boolean { + if (!post || isSystemMessage(post)) { + return false; + } + + const isOwner = isPostOwner(userId, post); + let canEdit = true; + + if (hasNewPermissions(state)) { + canEdit = canEdit && haveIChannelPermission(state, {team: teamId, channel: channelId, permission: Permissions.EDIT_POST}); + if (!isOwner) { + canEdit = canEdit && haveIChannelPermission(state, {team: teamId, channel: channelId, permission: Permissions.EDIT_OTHERS_POSTS}); + } + if (license.IsLicensed === 'true' && config.PostEditTimeLimit !== '-1' && config.PostEditTimeLimit !== -1) { + const timeLeft = (post.create_at + (config.PostEditTimeLimit * 1000)) - Date.now(); + if (timeLeft <= 0) { + canEdit = false; + } + } + } else { + // Backwards compatibility with pre-advanced permissions config settings. + canEdit = isOwner && config.AllowEditPost !== 'never'; + if (config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT) { + const timeLeft = (post.create_at + (config.PostEditTimeLimit * 1000)) - Date.now(); + if (timeLeft <= 0) { + canEdit = false; + } + } + } + + return canEdit; +} + +export function getLastCreateAt(postsArray: Array): number { + const createAt = postsArray.map((p) => p.create_at); + + if (createAt.length) { + return Reflect.apply(Math.max, null, createAt); + } + + return 0; +} + +const joinLeavePostTypes = [ + Posts.POST_TYPES.JOIN_LEAVE, + Posts.POST_TYPES.JOIN_CHANNEL, + Posts.POST_TYPES.LEAVE_CHANNEL, + Posts.POST_TYPES.ADD_REMOVE, + Posts.POST_TYPES.ADD_TO_CHANNEL, + Posts.POST_TYPES.REMOVE_FROM_CHANNEL, + Posts.POST_TYPES.JOIN_TEAM, + Posts.POST_TYPES.LEAVE_TEAM, + Posts.POST_TYPES.ADD_TO_TEAM, + Posts.POST_TYPES.REMOVE_FROM_TEAM, + Posts.POST_TYPES.COMBINED_USER_ACTIVITY, +]; + +// Returns true if a post should be hidden when the user has Show Join/Leave Messages disabled +export function shouldFilterJoinLeavePost(post: Post, showJoinLeave: boolean, currentUsername: string): boolean { + if (showJoinLeave) { + return false; + } + + // Don't filter out non-join/leave messages + if (joinLeavePostTypes.indexOf(post.type) === -1) { + return false; + } + + // Don't filter out join/leave messages about the current user + return !isJoinLeavePostForUsername(post, currentUsername); +} + +function isJoinLeavePostForUsername(post: Post, currentUsername: string): boolean { + if (!post.props || !currentUsername) { + return false; + } + + if (post.user_activity_posts) { + for (const childPost of post.user_activity_posts) { + if (isJoinLeavePostForUsername(childPost, currentUsername)) { + // If any of the contained posts are for this user, the client will + // need to figure out how to render the post + return true; + } + } + } + + return post.props.username === currentUsername || + post.props.addedUsername === currentUsername || + post.props.removedUsername === currentUsername; +} + +export function isPostPendingOrFailed(post: Post): boolean { + return post.failed || post.id === post.pending_post_id; +} + +export function comparePosts(a: Post, b: Post): number { + const aIsPendingOrFailed = isPostPendingOrFailed(a); + const bIsPendingOrFailed = isPostPendingOrFailed(b); + if (aIsPendingOrFailed && !bIsPendingOrFailed) { + return -1; + } else if (!aIsPendingOrFailed && bIsPendingOrFailed) { + return 1; + } + + if (a.create_at > b.create_at) { + return -1; + } else if (a.create_at < b.create_at) { + return 1; + } + + return 0; +} + +export function isPostCommentMention({post, currentUser, threadRepliedToByCurrentUser, rootPost}: {post: Post; currentUser: UserProfile; threadRepliedToByCurrentUser: boolean; rootPost: Post}): boolean { + let commentsNotifyLevel = Preferences.COMMENTS_NEVER; + let isCommentMention = false; + let threadCreatedByCurrentUser = false; + + if (rootPost && rootPost.user_id === currentUser.id) { + threadCreatedByCurrentUser = true; + } + if (currentUser.notify_props && currentUser.notify_props.comments) { + commentsNotifyLevel = currentUser.notify_props.comments; + } + + const notCurrentUser = post.user_id !== currentUser.id || (post.props && post.props.from_webhook); + if (notCurrentUser) { + if (commentsNotifyLevel === Preferences.COMMENTS_ANY && (threadCreatedByCurrentUser || threadRepliedToByCurrentUser)) { + isCommentMention = true; + } else if (commentsNotifyLevel === Preferences.COMMENTS_ROOT && threadCreatedByCurrentUser) { + isCommentMention = true; + } + } + return isCommentMention; +} + +export function fromAutoResponder(post: Post): boolean { + return Boolean(post.type && (post.type === Posts.SYSTEM_AUTO_RESPONDER)); +} + +export function getEmbedFromMetadata(metadata: PostMetadata): PostEmbed | null { + if (!metadata || !metadata.embeds || metadata.embeds.length === 0) { + return null; + } + + return metadata.embeds[0]; +} diff --git a/app/mm-redux/utils/preference_utils.ts b/app/mm-redux/utils/preference_utils.ts new file mode 100644 index 000000000..cbf2b7a21 --- /dev/null +++ b/app/mm-redux/utils/preference_utils.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Preferences} from '../constants'; + +import {PreferencesType} from '@mm-redux/types/preferences'; + +export function getPreferenceKey(category: string, name: string): string { + return `${category}--${name}`; +} + +export function getPreferencesByCategory(myPreferences: PreferencesType, category: string): Map { + const prefix = `${category}--`; + const preferences = new Map(); + Object.keys(myPreferences).forEach((key) => { + if (key.startsWith(prefix)) { + preferences.set(key.substring(prefix.length), myPreferences[key]); + } + }); + + return preferences; +} + +export function isChannelFavorite(myPreferences: PreferencesType, channelId: string): boolean { + const preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_FAVORITE_CHANNEL, channelId)]; + + return Boolean(preference && preference.value !== 'false'); +} diff --git a/app/mm-redux/utils/sentry.test.js b/app/mm-redux/utils/sentry.test.js new file mode 100644 index 000000000..663583b9f --- /dev/null +++ b/app/mm-redux/utils/sentry.test.js @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import Client4 from '@mm-redux/client/client4'; + +import {cleanUrlForLogging} from '@mm-redux/utils/sentry'; + +describe('utils/sentry', () => { + describe('cleanUrlForLogging', () => { + const baseUrl = 'https://mattermost.example.com/subpath'; + + const client = new Client4(); + client.setUrl(baseUrl); + + const tests = [{ + name: 'should remove server URL', + input: client.getUserRoute('me'), + expected: `${client.urlVersion}/users/me`, + }, { + name: 'should filter user IDs', + input: client.getUserRoute('1234'), + expected: `${client.urlVersion}/users/`, + }, { + name: 'should filter email addresses', + input: `${client.getUsersRoute()}/email/test@example.com`, + expected: `${client.urlVersion}/users/email/`, + }, { + name: 'should filter query parameters', + input: `${client.getUserRoute('me')}?foo=bar`, + expected: `${client.urlVersion}/users/me?`, + }]; + + for (const test of tests) { + it(test.name, () => { + assert.equal(cleanUrlForLogging(baseUrl, test.input), test.expected); + }); + } + }); +}); diff --git a/app/mm-redux/utils/sentry.ts b/app/mm-redux/utils/sentry.ts new file mode 100644 index 000000000..c69a29da0 --- /dev/null +++ b/app/mm-redux/utils/sentry.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// Given a URL from an API request, return a URL that has any parts removed that are either sensitive or that would +// prevent properly grouping the messages in Sentry. +export function cleanUrlForLogging(baseUrl: string, apiUrl: string): string { + let url = apiUrl; + + // Trim the host name + url = url.substring(baseUrl.length); + + // Filter the query string + const index = url.indexOf('?'); + if (index !== -1) { + url = url.substring(0, index); + } + + // A non-exhaustive whitelist to exclude parts of the URL that are unimportant (eg IDs) or may be sentsitive + // (eg email addresses). We prefer filtering out fields that aren't recognized because there should generally + // be enough left over for debugging. + // + // Note that new API routes don't need to be added here since this shouldn't be happening for newly added routes. + const whitelist = [ + 'api', 'v4', 'users', 'teams', 'scheme', 'name', 'members', 'channels', 'posts', 'reactions', 'commands', + 'files', 'preferences', 'hooks', 'incoming', 'outgoing', 'oauth', 'apps', 'emoji', 'brand', 'image', + 'data_retention', 'jobs', 'plugins', 'roles', 'system', 'timezones', 'schemes', 'redirect_location', 'patch', + 'mfa', 'password', 'reset', 'send', 'active', 'verify', 'terms_of_service', 'login', 'logout', 'ids', + 'usernames', 'me', 'username', 'email', 'default', 'sessions', 'revoke', 'all', 'audits', 'device', 'status', + 'search', 'switch', 'authorized', 'authorize', 'deauthorize', 'tokens', 'disable', 'enable', 'exists', 'unread', + 'invite', 'batch', 'stats', 'import', 'schemeRoles', 'direct', 'group', 'convert', 'view', 'search_autocomplete', + 'thread', 'info', 'flagged', 'pinned', 'pin', 'unpin', 'opengraph', 'actions', 'thumbnail', 'preview', 'link', + 'delete', 'logs', 'ping', 'config', 'client', 'license', 'websocket', 'webrtc', 'token', 'regen_token', + 'autocomplete', 'execute', 'regen_secret', 'policy', 'type', 'cancel', 'reload', 'environment', 's3_test', 'file', + 'caches', 'invalidate', 'database', 'recycle', 'compliance', 'reports', 'cluster', 'ldap', 'test', 'sync', 'saml', + 'certificate', 'public', 'private', 'idp', 'elasticsearch', 'purge_indexes', 'analytics', 'old', 'webapp', 'fake', + ]; + + url = url.split('/').map((part) => { + if (part !== '' && whitelist.indexOf(part) === -1) { + return ''; + } + + return part; + }).join('/'); + + if (index !== -1) { + // Add this on afterwards since it wouldn't pass the whitelist + url += '?'; + } + + return url; +} diff --git a/app/mm-redux/utils/team_utils.ts b/app/mm-redux/utils/team_utils.ts new file mode 100644 index 000000000..e00590213 --- /dev/null +++ b/app/mm-redux/utils/team_utils.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Team} from '@mm-redux/types/teams'; +import {IDMappedObjects, Dictionary} from '@mm-redux/types/utilities'; +import {General} from '../constants'; + +export function teamListToMap(teamList: Array): IDMappedObjects { + const teams: Dictionary = {}; + for (let i = 0; i < teamList.length; i++) { + teams[teamList[i].id] = teamList[i]; + } + return teams; +} + +export function sortTeamsWithLocale(locale: string): (a: Team, b: Team) => number { + return (a: Team, b: Team): number => { + if (a.display_name !== b.display_name) { + return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale || General.DEFAULT_LOCALE, {numeric: true}); + } + + return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale || General.DEFAULT_LOCALE, {numeric: true}); + }; +} diff --git a/app/mm-redux/utils/theme_utils.test.js b/app/mm-redux/utils/theme_utils.test.js new file mode 100644 index 000000000..f6a473e2f --- /dev/null +++ b/app/mm-redux/utils/theme_utils.test.js @@ -0,0 +1,89 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {Preferences} from '../constants'; +import * as ThemeUtils from '@mm-redux/utils/theme_utils'; + +describe('ThemeUtils', () => { + describe('getComponents', () => { + it('hex color', () => { + const input = 'ff77aa'; + const expected = {red: 255, green: 119, blue: 170, alpha: 1}; + + assert.deepEqual(ThemeUtils.getComponents(input), expected); + }); + + it('3 digit hex color', () => { + const input = '4a3'; + const expected = {red: 68, green: 170, blue: 51, alpha: 1}; + + assert.deepEqual(ThemeUtils.getComponents(input), expected); + }); + + it('hex color with leading number sign', () => { + const input = '#cda43d'; + const expected = {red: 205, green: 164, blue: 61, alpha: 1}; + + assert.deepEqual(ThemeUtils.getComponents(input), expected); + }); + + it('rgb', () => { + const input = 'rgb(123,231,67)'; + const expected = {red: 123, green: 231, blue: 67, alpha: 1}; + + assert.deepEqual(ThemeUtils.getComponents(input), expected); + }); + + it('rgba', () => { + const input = 'rgb(45,67,89,0.04)'; + const expected = {red: 45, green: 67, blue: 89, alpha: 0.04}; + + assert.deepEqual(ThemeUtils.getComponents(input), expected); + }); + }); + + describe('changeOpacity', () => { + it('hex color', () => { + const input = 'ff77aa'; + const expected = 'rgba(255,119,170,0.5)'; + + assert.deepEqual(ThemeUtils.changeOpacity(input, 0.5), expected); + }); + + it('rgb', () => { + const input = 'rgb(123,231,67)'; + const expected = 'rgba(123,231,67,0.3)'; + + assert.deepEqual(ThemeUtils.changeOpacity(input, 0.3), expected); + }); + + it('rgba', () => { + const input = 'rgb(45,67,89,0.4)'; + const expected = 'rgba(45,67,89,0.2)'; + + assert.deepEqual(ThemeUtils.changeOpacity(input, 0.5), expected); + }); + }); + + describe('setThemeDefaults', () => { + it('blank theme', () => { + const input = {}; + const expected = {...Preferences.THEMES.default}; + delete expected.type; + + assert.deepEqual(ThemeUtils.setThemeDefaults(input), expected); + }); + + it('set defaults on unset properties only', () => { + const input = {buttonColor: 'green'}; + assert.equal(ThemeUtils.setThemeDefaults(input).buttonColor, 'green'); + }); + + it('ignore type', () => { + const input = {type: 'sometype'}; + assert.equal(ThemeUtils.setThemeDefaults(input).type, 'sometype'); + }); + }); +}); diff --git a/app/mm-redux/utils/theme_utils.ts b/app/mm-redux/utils/theme_utils.ts new file mode 100644 index 000000000..0ebd89526 --- /dev/null +++ b/app/mm-redux/utils/theme_utils.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Theme} from '@mm-redux/types/preferences'; +import {Preferences} from '../constants'; + +export function makeStyleFromTheme(getStyleFromTheme: (a: any) => any): (a: any) => any { + let lastTheme: any; + let style: any; + return (theme: any) => { + if (!style || theme !== lastTheme) { + style = getStyleFromTheme(theme); + lastTheme = theme; + } + + return style; + }; +} + +const rgbPattern = /^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/; + +export function getComponents(inColor: string): {red: number; green: number; blue: number; alpha: number} { + let color = inColor; + + // RGB color + const match = rgbPattern.exec(color); + if (match) { + return { + red: parseInt(match[1], 10), + green: parseInt(match[2], 10), + blue: parseInt(match[3], 10), + alpha: match[4] ? parseFloat(match[4]) : 1, + }; + } + + // Hex color + if (color[0] === '#') { + color = color.slice(1); + } + + if (color.length === 3) { + const tempColor = color; + color = ''; + + color += tempColor[0] + tempColor[0]; + color += tempColor[1] + tempColor[1]; + color += tempColor[2] + tempColor[2]; + } + + return { + red: parseInt(color.substring(0, 2), 16), + green: parseInt(color.substring(2, 4), 16), + blue: parseInt(color.substring(4, 6), 16), + alpha: 1, + }; +} + +export function changeOpacity(oldColor: string, opacity: number): string { + const { + red, + green, + blue, + alpha, + } = getComponents(oldColor); + + return `rgba(${red},${green},${blue},${alpha * opacity})`; +} + +function blendComponent(background: number, foreground: number, opacity: number): number { + return ((1 - opacity) * background) + (opacity * foreground); +} + +export function blendColors(background: string, foreground: string, opacity: number): string { + const backgroundComponents = getComponents(background); + const foregroundComponents = getComponents(foreground); + + const red = Math.floor(blendComponent( + backgroundComponents.red, + foregroundComponents.red, + opacity + )); + const green = Math.floor(blendComponent( + backgroundComponents.green, + foregroundComponents.green, + opacity + )); + const blue = Math.floor(blendComponent( + backgroundComponents.blue, + foregroundComponents.blue, + opacity + )); + const alpha = blendComponent( + backgroundComponents.alpha, + foregroundComponents.alpha, + opacity + ); + + return `rgba(${red},${green},${blue},${alpha})`; +} + +// setThemeDefaults will set defaults on the theme for any unset properties. +export function setThemeDefaults(theme: Theme): Theme { + const defaultTheme = Preferences.THEMES.default; + + for (const property in defaultTheme) { + if (property === 'type') { + continue; + } + if (theme[property] == null) { + theme[property] = defaultTheme[property]; + } + } + + return theme; +} diff --git a/app/mm-redux/utils/timezone_utils.ts b/app/mm-redux/utils/timezone_utils.ts new file mode 100644 index 000000000..265d75b58 --- /dev/null +++ b/app/mm-redux/utils/timezone_utils.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {UserTimezone} from '@mm-redux/types/users'; +export function getUserCurrentTimezone(userTimezone?: UserTimezone): string | undefined | null { + if (!userTimezone) { + return null; + } + const { + useAutomaticTimezone, + automaticTimezone, + manualTimezone, + } = userTimezone; + + let useAutomatic = useAutomaticTimezone; + if (typeof useAutomaticTimezone === 'string') { + useAutomatic = useAutomaticTimezone === 'true'; + } + + if (useAutomatic) { + return automaticTimezone; + } + return manualTimezone; +} + +export function getTimezoneRegion(timezone: string): string { + if (timezone) { + const split = timezone.split('/'); + if (split.length > 1) { + return split.pop()!.replace(/_/g, ' '); + } + } + + return timezone; +} diff --git a/app/mm-redux/utils/user_utils.test.js b/app/mm-redux/utils/user_utils.test.js new file mode 100644 index 000000000..d6862a328 --- /dev/null +++ b/app/mm-redux/utils/user_utils.test.js @@ -0,0 +1,176 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {Preferences} from '../constants'; +import { + displayUsername, + filterProfilesMatchingTerm, + getSuggestionsSplitBy, + getSuggestionsSplitByMultiple, +} from '@mm-redux/utils/user_utils'; + +describe('user utils', () => { + describe('displayUsername', () => { + const userObj = { + id: 100, + username: 'testUser', + nickname: 'nick', + first_name: 'test', + last_name: 'user', + }; + const testUser1 = { + id: 'test_user_id', + username: 'username', + first_name: 'First', + last_name: 'Last', + }; + const testUser2 = { + id: 'test_user_id_2', + username: 'username2', + first_name: 'First2', + last_name: 'Last2', + nickname: 'nick2', + }; + it('should return username', () => { + assert.equal(displayUsername(userObj, 'UNKNOWN_PREFERENCE'), 'testUser'); + }); + + it('should return nickname', () => { + assert.equal(displayUsername(userObj, Preferences.DISPLAY_PREFER_NICKNAME), 'nick'); + }); + + it('should return fullname when no nick name', () => { + assert.equal(displayUsername({...userObj, nickname: ''}, Preferences.DISPLAY_PREFER_NICKNAME), 'test user'); + }); + + it('should return username when no nick name and no full name', () => { + assert.equal(displayUsername({...userObj, nickname: '', first_name: '', last_name: ''}, Preferences.DISPLAY_PREFER_NICKNAME), 'testUser'); + }); + + it('should return fullname', () => { + assert.equal(displayUsername(userObj, Preferences.DISPLAY_PREFER_FULL_NAME), 'test user'); + }); + + it('should return username when no full name', () => { + assert.equal(displayUsername({...userObj, first_name: '', last_name: ''}, Preferences.DISPLAY_PREFER_FULL_NAME), 'testUser'); + }); + + it('should return default username string', () => { + let noUserObj; + assert.equal(displayUsername(noUserObj, 'UNKNOWN_PREFERENCE'), 'Someone'); + }); + + it('should return empty string when user does not exist and useDefaultUserName param is false', () => { + let noUserObj; + assert.equal(displayUsername(noUserObj, 'UNKNOWN_PREFERENCE', false), ''); + }); + }); + + describe('filterProfilesMatchingTerm', () => { + const userA = { + id: 100, + username: 'testUser.split_10-', + nickname: 'nick', + first_name: 'First', + last_name: 'Last1', + }; + const userB = { + id: 101, + username: 'extraPerson-split', + nickname: 'somebody', + first_name: 'First', + last_name: 'Last2', + email: 'left@right.com', + }; + const users = [userA, userB]; + + it('should match all for empty filter', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, ''), [userA, userB]); + }); + + it('should filter out results which do not match', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'testBad'), []); + }); + + it('should match by username', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'testUser'), [userA]); + }); + + it('should match by split part of the username', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'split'), [userA, userB]); + assert.deepEqual(filterProfilesMatchingTerm(users, '10'), [userA]); + }); + + it('should match by firstname', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'First'), [userA, userB]); + }); + + it('should match by lastname prefix', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'Last'), [userA, userB]); + }); + + it('should match by lastname fully', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'Last2'), [userB]); + }); + + it('should match by fullname prefix', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'First Last'), [userA, userB]); + }); + + it('should match by fullname fully', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'First Last1'), [userA]); + }); + + it('should match by fullname case-insensitive', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'first LAST'), [userA, userB]); + }); + + it('should match by nickname', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'some'), [userB]); + }); + + it('should not match by nickname substring', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'body'), []); + }); + + it('should match by email prefix', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'left'), [userB]); + }); + + it('should match by email domain', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'right'), [userB]); + }); + + it('should match by full email', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, 'left@right.com'), [userB]); + }); + + it('should ignore leading @ for username', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, '@testUser'), [userA]); + }); + + it('should ignore leading @ for firstname', () => { + assert.deepEqual(filterProfilesMatchingTerm(users, '@first'), [userA, userB]); + }); + }); + + describe('Utils.getSuggestionsSplitBy', () => { + test('correct suggestions when splitting by a character', () => { + const term = 'one.two.three'; + const expectedSuggestions = ['one.two.three', '.two.three', 'two.three', '.three', 'three']; + + expect(getSuggestionsSplitBy(term, '.')).toEqual(expectedSuggestions); + }); + }); + + describe('Utils.getSuggestionsSplitByMultiple', () => { + test('correct suggestions when splitting by multiple characters', () => { + const term = 'one.two-three'; + const expectedSuggestions = ['one.two-three', '.two-three', 'two-three', '-three', 'three']; + + expect(getSuggestionsSplitByMultiple(term, ['.', '-'])).toEqual(expectedSuggestions); + }); + }); +}); diff --git a/app/mm-redux/utils/user_utils.ts b/app/mm-redux/utils/user_utils.ts new file mode 100644 index 000000000..e480d8179 --- /dev/null +++ b/app/mm-redux/utils/user_utils.ts @@ -0,0 +1,173 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {General, Preferences} from '../constants'; +import {localizeMessage} from '@mm-redux/utils/i18n_utils'; +import {UserProfile} from '@mm-redux/types/users'; +import {IDMappedObjects, $ID, Dictionary} from '@mm-redux/types/utilities'; +export function getFullName(user: UserProfile): string { + if (user.first_name && user.last_name) { + return user.first_name + ' ' + user.last_name; + } else if (user.first_name) { + return user.first_name; + } else if (user.last_name) { + return user.last_name; + } + + return ''; +} + +export function displayUsername( + user: UserProfile, + teammateNameDisplay: string, + useFallbackUsername = true, +): string { + let name = useFallbackUsername ? localizeMessage('channel_loader.someone', 'Someone') : ''; + if (user) { + if (teammateNameDisplay === Preferences.DISPLAY_PREFER_NICKNAME) { + name = user.nickname || getFullName(user); + } else if (teammateNameDisplay === Preferences.DISPLAY_PREFER_FULL_NAME) { + name = getFullName(user); + } else { + name = user.username; + } + + if (!name || name.trim().length === 0) { + name = user.username; + } + } + + return name; +} + +export function rolesIncludePermission(roles: string, permission: string): boolean { + const rolesArray = roles.split(' '); + return rolesArray.includes(permission); +} + +export function isAdmin(roles: string): boolean { + return isSystemAdmin(roles) || isTeamAdmin(roles); +} + +export function isGuest(roles: string): boolean { + return rolesIncludePermission(roles, 'system_guest'); +} + +export function isTeamAdmin(roles: string): boolean { + return rolesIncludePermission(roles, General.TEAM_ADMIN_ROLE); +} + +export function isSystemAdmin(roles: string): boolean { + return rolesIncludePermission(roles, General.SYSTEM_ADMIN_ROLE); +} + +export function isChannelAdmin(roles: string): boolean { + return rolesIncludePermission(roles, General.CHANNEL_ADMIN_ROLE); +} + +export function hasUserAccessTokenRole(roles: string): boolean { + return rolesIncludePermission(roles, General.SYSTEM_USER_ACCESS_TOKEN_ROLE); +} + +export function hasPostAllRole(roles: string): boolean { + return rolesIncludePermission(roles, General.SYSTEM_POST_ALL_ROLE); +} + +export function hasPostAllPublicRole(roles: string): boolean { + return rolesIncludePermission(roles, General.SYSTEM_POST_ALL_PUBLIC_ROLE); +} + +export function profileListToMap(profileList: Array): IDMappedObjects { + const profiles: Dictionary = {}; + for (let i = 0; i < profileList.length; i++) { + profiles[profileList[i].id] = profileList[i]; + } + return profiles; +} + +export function removeUserFromList(userId: $ID, list: Array): Array { + for (let i = list.length - 1; i >= 0; i--) { + if (list[i].id === userId) { + list.splice(i, 1); + return list; + } + } + + return list; +} + +// Splits the term by a splitStr and composes a list of the parts of +// the split concatenated with the rest, forming a set of suggesitons +// matchable with startsWith +// +// E.g.: for "one.two.three" by "." it would yield +// ["one.two.three", ".two.three", "two.three", ".three", "three"] +export function getSuggestionsSplitBy(term: string, splitStr: string): Array { + const splitTerm = term.split(splitStr); + const initialSuggestions = splitTerm.map((st, i) => splitTerm.slice(i).join(splitStr)); + let suggestions: string[] = []; + + if (splitStr === ' ') { + suggestions = initialSuggestions; + } else { + suggestions = initialSuggestions.reduce((acc, val) => { + if (acc.length === 0) { + acc.push(val); + } else { + acc.push(splitStr + val, val); + } + return acc; + }, [] as string[]); + } + return suggestions; +} + +export function getSuggestionsSplitByMultiple(term: string, splitStrs: Array): Array { + const suggestions = splitStrs.reduce((acc, val) => { + getSuggestionsSplitBy(term, val).forEach((suggestion) => acc.add(suggestion)); + return acc; + }, new Set()); + + return [...suggestions]; +} + +export function filterProfilesMatchingTerm(users: Array, term: string): Array { + const lowercasedTerm = term.toLowerCase(); + let trimmedTerm = lowercasedTerm; + if (trimmedTerm.startsWith('@')) { + trimmedTerm = trimmedTerm.substr(1); + } + + return users.filter((user: UserProfile) => { + if (!user) { + return false; + } + + const profileSuggestions: string[] = []; + const usernameSuggestions = getSuggestionsSplitByMultiple((user.username || '').toLowerCase(), General.AUTOCOMPLETE_SPLIT_CHARACTERS); + profileSuggestions.push(...usernameSuggestions); + const first = (user.first_name || '').toLowerCase(); + const last = (user.last_name || '').toLowerCase(); + const full = first + ' ' + last; + profileSuggestions.push(first, last, full); + profileSuggestions.push((user.nickname || '').toLowerCase()); + const email = (user.email || '').toLowerCase(); + profileSuggestions.push(email); + profileSuggestions.push((user.nickname || '').toLowerCase()); + + const split = email.split('@'); + if (split.length > 1) { + profileSuggestions.push(split[1]); + } + + return profileSuggestions. + filter((suggestion) => suggestion !== ''). + some((suggestion) => suggestion.startsWith(trimmedTerm)); + }); +} + +export function sortByUsername(a: UserProfile, b: UserProfile): number { + const nameA = a.username; + const nameB = b.username; + + return nameA.localeCompare(nameB); +} diff --git a/app/reducers/device/connection.js b/app/reducers/device/connection.js index 744596225..a25f75783 100644 --- a/app/reducers/device/connection.js +++ b/app/reducers/device/connection.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; import {DeviceTypes} from 'app/constants'; export default function connection(state = true, action) { diff --git a/app/reducers/device/dimension.js b/app/reducers/device/dimension.js index eb4ea0865..eea7ce361 100644 --- a/app/reducers/device/dimension.js +++ b/app/reducers/device/dimension.js @@ -3,7 +3,7 @@ import {Dimensions} from 'react-native'; -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; import {DeviceTypes} from 'app/constants'; const {height, width} = Dimensions.get('window'); diff --git a/app/reducers/navigation/index.js b/app/reducers/navigation/index.js index cebb69883..23b20ef1d 100644 --- a/app/reducers/navigation/index.js +++ b/app/reducers/navigation/index.js @@ -2,8 +2,8 @@ // See LICENSE.txt for license information. import {NavigationTypes} from 'app/constants'; -import {UserTypes} from 'mattermost-redux/action_types'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {UserTypes} from '@mm-redux/action_types'; +import EventEmitter from '@mm-redux/utils/event_emitter'; export default function(state = '', action) { switch (action.type) { diff --git a/app/reducers/views/announcement.js b/app/reducers/views/announcement.js index 2ab8073b4..e2be3726a 100644 --- a/app/reducers/views/announcement.js +++ b/app/reducers/views/announcement.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index bab509379..e365106bd 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -7,7 +7,7 @@ import { FileTypes, PostTypes, UserTypes, -} from 'mattermost-redux/action_types'; +} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/reducers/views/emoji.js b/app/reducers/views/emoji.js index 19ee2fad0..f32a47a89 100644 --- a/app/reducers/views/emoji.js +++ b/app/reducers/views/emoji.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {combineReducers} from 'redux'; -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/reducers/views/i18n.js b/app/reducers/views/i18n.js index f4a1cd2a0..d64683e05 100644 --- a/app/reducers/views/i18n.js +++ b/app/reducers/views/i18n.js @@ -4,7 +4,7 @@ import {combineReducers} from 'redux'; import DeviceInfo from 'react-native-device-info'; -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; const defaultLocale = DeviceInfo.getDeviceLocale().split('-')[0]; diff --git a/app/reducers/views/post.js b/app/reducers/views/post.js index 04e9d64b7..92bbcc96c 100644 --- a/app/reducers/views/post.js +++ b/app/reducers/views/post.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {combineReducers} from 'redux'; -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/reducers/views/root.js b/app/reducers/views/root.js index e66408d2e..e7aaba0a2 100644 --- a/app/reducers/views/root.js +++ b/app/reducers/views/root.js @@ -3,7 +3,7 @@ import {combineReducers} from 'redux'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import {ViewTypes} from 'app/constants'; diff --git a/app/reducers/views/search.js b/app/reducers/views/search.js index d7e16bdeb..a4645cc70 100644 --- a/app/reducers/views/search.js +++ b/app/reducers/views/search.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {ViewTypes} from 'app/constants'; -import {UserTypes} from 'mattermost-redux/action_types'; +import {UserTypes} from '@mm-redux/action_types'; export default function search(state = '', action) { switch (action.type) { diff --git a/app/reducers/views/team.js b/app/reducers/views/team.js index 6eca847ca..a1c95d026 100644 --- a/app/reducers/views/team.js +++ b/app/reducers/views/team.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {combineReducers} from 'redux'; -import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types'; +import {ChannelTypes, TeamTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/reducers/views/thread.js b/app/reducers/views/thread.js index 327d80e67..952c72171 100644 --- a/app/reducers/views/thread.js +++ b/app/reducers/views/thread.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {combineReducers} from 'redux'; -import {FileTypes, PostTypes} from 'mattermost-redux/action_types'; +import {FileTypes, PostTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/screens/about/index.js b/app/screens/about/index.js index f2a3b69ff..11e40ac97 100644 --- a/app/screens/about/index.js +++ b/app/screens/about/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; import {isLandscape} from 'app/selectors/device'; import About from './about'; diff --git a/app/screens/channel/channel.android.js b/app/screens/channel/channel.android.js index 761c990a6..c6123fff0 100644 --- a/app/screens/channel/channel.android.js +++ b/app/screens/channel/channel.android.js @@ -4,7 +4,7 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {openMainSideMenu, openSettingsSideMenu} from 'app/actions/navigation'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index 985a6cbc0..a865b5bd3 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -11,7 +11,7 @@ import { } from 'react-native'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {showModal, showModalOverCurrentContext} from 'app/actions/navigation'; import SafeAreaView from 'app/components/safe_area_view'; diff --git a/app/screens/channel/channel_base.test.js b/app/screens/channel/channel_base.test.js index bdd0b6a05..975d56703 100644 --- a/app/screens/channel/channel_base.test.js +++ b/app/screens/channel/channel_base.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import EphemeralStore from 'app/store/ephemeral_store'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js index 614b26653..6f19f1da3 100644 --- a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js +++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js @@ -4,7 +4,7 @@ import React from 'react'; import NotificationsIOS from 'react-native-notifications'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import Badge from 'app/components/badge'; import PushNotification from 'app/push_notifications/push_notifications.ios'; diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js index 714aaf2e1..6590203d0 100644 --- a/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js +++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getBadgeCount} from 'app/selectors/views'; import ChannelDrawerButton from './channel_drawer_button'; diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 532c68b68..0e3da81cc 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {Dimensions, Platform, View} from 'react-native'; import AsyncStorage from '@react-native-community/async-storage'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {DeviceTypes, ViewTypes} from 'app/constants'; import mattermostManaged from 'app/mattermost_managed'; diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js index 88a63b1e6..19b00404b 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {DeviceTypes} from 'app/constants'; diff --git a/app/screens/channel/channel_nav_bar/channel_search_button/index.js b/app/screens/channel/channel_nav_bar/channel_search_button/index.js index 920322f7d..c7eacae89 100644 --- a/app/screens/channel/channel_nav_bar/channel_search_button/index.js +++ b/app/screens/channel/channel_nav_bar/channel_search_button/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {clearSearch} from 'mattermost-redux/actions/search'; +import {clearSearch} from '@mm-redux/actions/search'; import ChannelSearchButton from './channel_search_button'; diff --git a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js index 3b305a9b2..be8404838 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js +++ b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js @@ -13,7 +13,7 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import {t} from 'app/utils/i18n'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import FormattedText from 'app/components/formatted_text'; export default class ChannelTitle extends PureComponent { diff --git a/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js b/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js index 8cc129bdd..2b253751d 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js +++ b/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import ChannelTitle from './channel_title'; diff --git a/app/screens/channel/channel_nav_bar/channel_title/index.js b/app/screens/channel/channel_nav_bar/channel_title/index.js index 8795c3b89..eb749515c 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/index.js +++ b/app/screens/channel/channel_nav_bar/channel_title/index.js @@ -3,11 +3,11 @@ import {connect} from 'react-redux'; -import {General} from 'mattermost-redux/constants'; -import {getCurrentChannel, getMyCurrentChannelMembership, getCurrentChannelStats} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; -import {getUserIdFromChannelName, isChannelMuted} from 'mattermost-redux/utils/channel_utils'; +import {General} from '@mm-redux/constants'; +import {getCurrentChannel, getMyCurrentChannelMembership, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; +import {getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils'; import {isGuest} from 'app/utils/users'; diff --git a/app/screens/channel/channel_nav_bar/index.js b/app/screens/channel/channel_nav_bar/index.js index 678be0af5..427c8e6aa 100644 --- a/app/screens/channel/channel_nav_bar/index.js +++ b/app/screens/channel/channel_nav_bar/index.js @@ -4,7 +4,7 @@ import {connect} from 'react-redux'; import {isLandscape} from 'app/selectors/device'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import ChannelNavBar from './channel_nav_bar'; diff --git a/app/screens/channel/channel_nav_bar/settings_drawer_button.js b/app/screens/channel/channel_nav_bar/settings_drawer_button.js index 2a2a52bf1..f39dbd9e1 100644 --- a/app/screens/channel/channel_nav_bar/settings_drawer_button.js +++ b/app/screens/channel/channel_nav_bar/settings_drawer_button.js @@ -11,7 +11,7 @@ import { import Icon from 'react-native-vector-icons/Ionicons'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {preventDoubleTap} from 'app/utils/tap'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 3f47d87dc..3fbe4b89c 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -9,8 +9,8 @@ import { View, } from 'react-native'; -import {getLastPostIndex} from 'mattermost-redux/utils/post_list'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {getLastPostIndex} from '@mm-redux/utils/post_list'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import AnnouncementBanner from 'app/components/announcement_banner'; import PostList from 'app/components/post_list'; diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index 81be85aec..ee88eecad 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -4,11 +4,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectPost} from 'mattermost-redux/actions/posts'; -import {getPostIdsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {selectPost} from '@mm-redux/actions/posts'; +import {getPostIdsInCurrentChannel} from '@mm-redux/selectors/entities/posts'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import { loadPostsIfNecessaryWithRetry, diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index a28d2aa67..e061a0ab8 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -4,12 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users'; -import {getChannelStats} from 'mattermost-redux/actions/channels'; +import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from '@mm-redux/actions/users'; +import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {shouldShowTermsOfService} from '@mm-redux/selectors/entities/users'; +import {getChannelStats} from '@mm-redux/actions/channels'; import { loadChannelsForTeam, diff --git a/app/screens/channel_add_members/channel_add_members.js b/app/screens/channel_add_members/channel_add_members.js index d2adaff58..5bee4b0fa 100644 --- a/app/screens/channel_add_members/channel_add_members.js +++ b/app/screens/channel_add_members/channel_add_members.js @@ -11,9 +11,9 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {debounce} from 'mattermost-redux/actions/helpers'; -import {General} from 'mattermost-redux/constants'; -import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils'; +import {debounce} from '@mm-redux/actions/helpers'; +import {General} from '@mm-redux/constants'; +import {filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import Loading from 'app/components/loading'; diff --git a/app/screens/channel_add_members/channel_add_members.test.js b/app/screens/channel_add_members/channel_add_members.test.js index 8d83487dd..5ac8fcfc1 100644 --- a/app/screens/channel_add_members/channel_add_members.test.js +++ b/app/screens/channel_add_members/channel_add_members.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import * as NavigationActions from 'app/actions/navigation'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/channel_add_members/index.js b/app/screens/channel_add_members/index.js index 8d4582477..e8d2d47a3 100644 --- a/app/screens/channel_add_members/index.js +++ b/app/screens/channel_add_members/index.js @@ -4,12 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTeamStats} from 'mattermost-redux/actions/teams'; -import {getProfilesNotInChannel, searchProfiles} from 'mattermost-redux/actions/users'; -import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; +import {getTeamStats} from '@mm-redux/actions/teams'; +import {getProfilesNotInChannel, searchProfiles} from '@mm-redux/actions/users'; +import {getCurrentChannel} from '@mm-redux/selectors/entities/channels'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId, getProfilesNotInCurrentChannel} from '@mm-redux/selectors/entities/users'; import {handleAddChannelMembers} from 'app/actions/views/channel_add_members'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index 09dc8a6c8..91b6f8528 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -11,7 +11,7 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {General, Users} from 'mattermost-redux/constants'; +import {General, Users} from '@mm-redux/constants'; import StatusBar from 'app/components/status_bar'; import {preventDoubleTap} from 'app/utils/tap'; diff --git a/app/screens/channel_info/channel_info.test.js b/app/screens/channel_info/channel_info.test.js index bad5f6a3c..be326b699 100644 --- a/app/screens/channel_info/channel_info.test.js +++ b/app/screens/channel_info/channel_info.test.js @@ -3,8 +3,8 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; -import {General} from 'mattermost-redux/constants'; +import Preferences from '@mm-redux/constants/preferences'; +import {General} from '@mm-redux/constants'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index 4766d0af6..659b78191 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import ChannelIcon from 'app/components/channel_icon'; import FormattedDate from 'app/components/formatted_date'; diff --git a/app/screens/channel_info/channel_info_header.test.js b/app/screens/channel_info/channel_info_header.test.js index 94ec048c5..08c7c4c36 100644 --- a/app/screens/channel_info/channel_info_header.test.js +++ b/app/screens/channel_info/channel_info_header.test.js @@ -3,8 +3,8 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; -import {General} from 'mattermost-redux/constants'; +import Preferences from '@mm-redux/constants/preferences'; +import {General} from '@mm-redux/constants'; import ChannelInfoHeader from './channel_info_header.js'; diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index a699f0547..395add4aa 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -13,12 +13,12 @@ import { unarchiveChannel, unfavoriteChannel, updateChannelNotifyProps, -} from 'mattermost-redux/actions/channels'; -import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis'; -import {selectFocusedPostId} from 'mattermost-redux/actions/posts'; -import {clearPinnedPosts} from 'mattermost-redux/actions/search'; -import {General} from 'mattermost-redux/constants'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +} from '@mm-redux/actions/channels'; +import {getCustomEmojisInText} from '@mm-redux/actions/emojis'; +import {selectFocusedPostId} from '@mm-redux/actions/posts'; +import {clearPinnedPosts} from '@mm-redux/actions/search'; +import {General} from '@mm-redux/constants'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import { canManageChannelMembers, getCurrentChannel, @@ -26,17 +26,17 @@ import { getSortedFavoriteChannelIds, getMyCurrentChannelMembership, isCurrentChannelReadOnly, -} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; -import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils'; -import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; -import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; -import Permissions from 'mattermost-redux/constants/permissions'; -import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from '@mm-redux/selectors/entities/users'; +import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from '@mm-redux/utils/channel_utils'; +import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils'; +import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; +import Permissions from '@mm-redux/constants/permissions'; +import {haveITeamPermission} from '@mm-redux/selectors/entities/roles'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import { closeDMChannel, diff --git a/app/screens/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js index 6f01936ce..d6c74469d 100644 --- a/app/screens/channel_members/channel_members.js +++ b/app/screens/channel_members/channel_members.js @@ -11,9 +11,9 @@ import { import {intlShape} from 'react-intl'; import {Navigation} from 'react-native-navigation'; -import {debounce} from 'mattermost-redux/actions/helpers'; -import {General} from 'mattermost-redux/constants'; -import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils'; +import {debounce} from '@mm-redux/actions/helpers'; +import {General} from '@mm-redux/constants'; +import {filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import Loading from 'app/components/loading'; diff --git a/app/screens/channel_members/channel_members.test.js b/app/screens/channel_members/channel_members.test.js index 90cbdf641..29f0ce193 100644 --- a/app/screens/channel_members/channel_members.test.js +++ b/app/screens/channel_members/channel_members.test.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/channel_members/index.js b/app/screens/channel_members/index.js index 96f6d0362..31e9bbd0e 100644 --- a/app/screens/channel_members/index.js +++ b/app/screens/channel_members/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; -import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users'; -import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentChannel, canManageChannelMembers} from '@mm-redux/selectors/entities/channels'; +import {makeGetProfilesInChannel} from '@mm-redux/selectors/entities/users'; +import {getProfilesInChannel, searchProfiles} from '@mm-redux/actions/users'; import {handleRemoveChannelMembers} from 'app/actions/views/channel_members'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/screens/client_upgrade/index.js b/app/screens/client_upgrade/index.js index b0984934c..630036d24 100644 --- a/app/screens/client_upgrade/index.js +++ b/app/screens/client_upgrade/index.js @@ -3,11 +3,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {logError} from 'mattermost-redux/actions/errors'; +import {logError} from '@mm-redux/actions/errors'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import getClientUpgrade from 'app/selectors/client_upgrade'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import ClientUpgrade from './client_upgrade'; diff --git a/app/screens/code/index.js b/app/screens/code/index.js index 4b0abdcd9..a8760efbb 100644 --- a/app/screens/code/index.js +++ b/app/screens/code/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import Code from './code'; diff --git a/app/screens/create_channel/create_channel.js b/app/screens/create_channel/create_channel.js index da0d57d89..c0ee07021 100644 --- a/app/screens/create_channel/create_channel.js +++ b/app/screens/create_channel/create_channel.js @@ -10,8 +10,8 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {General, RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {General, RequestStatus} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {popTopScreen, dismissModal, setButtons} from 'app/actions/navigation'; import EditChannelInfo from 'app/components/edit_channel_info'; diff --git a/app/screens/create_channel/index.js b/app/screens/create_channel/index.js index e3111d7cb..ba0110c09 100644 --- a/app/screens/create_channel/index.js +++ b/app/screens/create_channel/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {handleCreateChannel} from 'app/actions/views/create_channel'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/screens/edit_channel/edit_channel.js b/app/screens/edit_channel/edit_channel.js index 8d4fa7ebf..a384940b1 100644 --- a/app/screens/edit_channel/edit_channel.js +++ b/app/screens/edit_channel/edit_channel.js @@ -10,8 +10,8 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; import SafeAreaView from 'app/components/safe_area_view'; -import {General, RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {General, RequestStatus} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import EditChannelInfo from 'app/components/edit_channel_info'; import {NavigationTypes, ViewTypes} from 'app/constants'; diff --git a/app/screens/edit_channel/index.js b/app/screens/edit_channel/index.js index 4a0eb2182..22e207a65 100644 --- a/app/screens/edit_channel/index.js +++ b/app/screens/edit_channel/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams'; -import {patchChannel, getChannel} from 'mattermost-redux/actions/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentChannel} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamUrl} from '@mm-redux/selectors/entities/teams'; +import {patchChannel, getChannel} from '@mm-redux/actions/channels'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {setChannelDisplayName} from 'app/actions/views/channel'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/screens/edit_post/edit_post.js b/app/screens/edit_post/edit_post.js index 567c8cc8a..e1dbaa7f3 100644 --- a/app/screens/edit_post/edit_post.js +++ b/app/screens/edit_post/edit_post.js @@ -10,7 +10,7 @@ import { import {Navigation} from 'react-native-navigation'; import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view'; -import {RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from '@mm-redux/constants'; import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete'; import ErrorText from 'app/components/error_text'; diff --git a/app/screens/edit_post/index.js b/app/screens/edit_post/index.js index 877c089ba..c9cfb0697 100644 --- a/app/screens/edit_post/index.js +++ b/app/screens/edit_post/index.js @@ -4,8 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {editPost} from 'mattermost-redux/actions/posts'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {editPost} from '@mm-redux/actions/posts'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getDimensions, isLandscape} from 'app/selectors/device'; diff --git a/app/screens/edit_profile/edit_profile.js b/app/screens/edit_profile/edit_profile.js index 3ba8c0a7d..01c9f88e6 100644 --- a/app/screens/edit_profile/edit_profile.js +++ b/app/screens/edit_profile/edit_profile.js @@ -10,8 +10,8 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import DocumentPicker from 'react-native-document-picker'; import {Navigation} from 'react-native-navigation'; -import {Client4} from 'mattermost-redux/client'; -import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils'; +import {Client4} from '@mm-redux/client'; +import {getFormattedFileSize} from '@mm-redux/utils/file_utils'; import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file'; import {emptyFunction} from 'app/utils/general'; diff --git a/app/screens/edit_profile/edit_profile.test.js b/app/screens/edit_profile/edit_profile.test.js index 1e6765b04..e0648f5bc 100644 --- a/app/screens/edit_profile/edit_profile.test.js +++ b/app/screens/edit_profile/edit_profile.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import EditProfile from './edit_profile.js'; diff --git a/app/screens/edit_profile/index.js b/app/screens/edit_profile/index.js index 420166ec5..47d32d44c 100644 --- a/app/screens/edit_profile/index.js +++ b/app/screens/edit_profile/index.js @@ -4,9 +4,9 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {isLandscape} from 'app/selectors/device'; import {setProfileImageUri, removeProfileImage, updateUser} from 'app/actions/views/edit_profile'; diff --git a/app/screens/error_teams_list/error_teams_list.test.js b/app/screens/error_teams_list/error_teams_list.test.js index e6fcaf04c..d85e1e4a8 100644 --- a/app/screens/error_teams_list/error_teams_list.test.js +++ b/app/screens/error_teams_list/error_teams_list.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import FailedNetworkAction from 'app/components/failed_network_action'; import ErrorTeamsList from './error_teams_list'; diff --git a/app/screens/expanded_announcement_banner/index.js b/app/screens/expanded_announcement_banner/index.js index 83837c9c3..715040bd3 100644 --- a/app/screens/expanded_announcement_banner/index.js +++ b/app/screens/expanded_announcement_banner/index.js @@ -4,8 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {dismissBanner} from 'app/actions/views/announcement'; diff --git a/app/screens/flagged_posts/flagged_posts.js b/app/screens/flagged_posts/flagged_posts.js index 58d596536..ab4677778 100644 --- a/app/screens/flagged_posts/flagged_posts.js +++ b/app/screens/flagged_posts/flagged_posts.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {isDateLine, getDateForDateLine} from 'mattermost-redux/utils/post_list'; +import {isDateLine, getDateForDateLine} from '@mm-redux/utils/post_list'; import ChannelLoader from 'app/components/channel_loader'; import DateHeader from 'app/components/post_list/date_header'; diff --git a/app/screens/flagged_posts/flagged_posts.test.js b/app/screens/flagged_posts/flagged_posts.test.js index ae731a9d2..1457816eb 100644 --- a/app/screens/flagged_posts/flagged_posts.test.js +++ b/app/screens/flagged_posts/flagged_posts.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/flagged_posts/index.js b/app/screens/flagged_posts/index.js index d8f837f67..2aae50935 100644 --- a/app/screens/flagged_posts/index.js +++ b/app/screens/flagged_posts/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts'; -import {clearSearch, getFlaggedPosts} from 'mattermost-redux/actions/search'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {selectFocusedPostId, selectPost} from '@mm-redux/actions/posts'; +import {clearSearch, getFlaggedPosts} from '@mm-redux/actions/search'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; diff --git a/app/screens/forgot_password/forgot_password.js b/app/screens/forgot_password/forgot_password.js index a6bb36e20..bcc372bab 100644 --- a/app/screens/forgot_password/forgot_password.js +++ b/app/screens/forgot_password/forgot_password.js @@ -15,7 +15,7 @@ import { View, } from 'react-native'; -import {isEmail} from 'mattermost-redux/utils/helpers'; +import {isEmail} from '@mm-redux/utils/helpers'; import {GlobalStyles} from 'app/styles'; diff --git a/app/screens/forgot_password/index.js b/app/screens/forgot_password/index.js index 612e7c00f..3413bb5b6 100644 --- a/app/screens/forgot_password/index.js +++ b/app/screens/forgot_password/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {sendPasswordResetEmail} from 'mattermost-redux/actions/users'; +import {sendPasswordResetEmail} from '@mm-redux/actions/users'; import ForgotPassword from './forgot_password'; function mapDispatchToProps(dispatch) { diff --git a/app/screens/image_preview/downloader.android.js b/app/screens/image_preview/downloader.android.js index 9466ff1ae..02647b266 100644 --- a/app/screens/image_preview/downloader.android.js +++ b/app/screens/image_preview/downloader.android.js @@ -13,7 +13,7 @@ import { import RNFetchBlob from 'rn-fetch-blob'; import {intlShape} from 'react-intl'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import {DeviceTypes} from 'app/constants/'; import FormattedText from 'app/components/formatted_text'; diff --git a/app/screens/image_preview/downloader.ios.js b/app/screens/image_preview/downloader.ios.js index 149f44065..aaa3408ed 100644 --- a/app/screens/image_preview/downloader.ios.js +++ b/app/screens/image_preview/downloader.ios.js @@ -10,7 +10,7 @@ import Icon from 'react-native-vector-icons/Ionicons'; import {intlShape} from 'react-intl'; import CameraRoll from '@react-native-community/cameraroll'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import FormattedText from 'app/components/formatted_text'; import mattermostBucket from 'app/mattermost_bucket'; diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js index a2da9b89e..01cdd8181 100644 --- a/app/screens/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -24,7 +24,7 @@ import Gallery from 'react-native-image-gallery'; import DeviceInfo from 'react-native-device-info'; import FastImage from 'react-native-fast-image'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import FileAttachmentDocument from 'app/components/file_attachment_list/file_attachment_document'; import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; diff --git a/app/screens/image_preview/image_preview.test.js b/app/screens/image_preview/image_preview.test.js index 4295022a0..28ec2fe0e 100644 --- a/app/screens/image_preview/image_preview.test.js +++ b/app/screens/image_preview/image_preview.test.js @@ -7,7 +7,7 @@ import {TouchableOpacity} from 'react-native'; // import RNFetchBlob from 'rn-fetch-blob'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/screens/image_preview/index.js b/app/screens/image_preview/index.js index c526f3475..be2ed4b01 100644 --- a/app/screens/image_preview/index.js +++ b/app/screens/image_preview/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {canDownloadFilesOnMobile} from '@mm-redux/selectors/entities/general'; import {getDimensions} from 'app/selectors/device'; diff --git a/app/screens/image_preview/video_preview.js b/app/screens/image_preview/video_preview.js index 872d060ef..c30edabfa 100644 --- a/app/screens/image_preview/video_preview.js +++ b/app/screens/image_preview/video_preview.js @@ -14,7 +14,7 @@ import Video from 'react-native-video'; import RNFetchBlob from 'rn-fetch-blob'; import {intlShape} from 'react-intl'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import VideoControls, {PLAYER_STATE} from 'app/components/video_controls'; import {DeviceTypes} from 'app/constants/'; diff --git a/app/screens/interactive_dialog/dialog_element.test.js b/app/screens/interactive_dialog/dialog_element.test.js index db08848f1..fde052355 100644 --- a/app/screens/interactive_dialog/dialog_element.test.js +++ b/app/screens/interactive_dialog/dialog_element.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import RadioSetting from 'app/components/widgets/settings/radio_setting'; import BoolSetting from 'app/components/widgets/settings/bool_setting'; import DialogElement from './dialog_element.js'; diff --git a/app/screens/interactive_dialog/dialog_introduction_text.test.js b/app/screens/interactive_dialog/dialog_introduction_text.test.js index 2e2a6e32d..cbc5f087c 100644 --- a/app/screens/interactive_dialog/dialog_introduction_text.test.js +++ b/app/screens/interactive_dialog/dialog_introduction_text.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import DialogIntroductionText from './dialog_introduction_text.js'; diff --git a/app/screens/interactive_dialog/index.js b/app/screens/interactive_dialog/index.js index 9c8bf17a7..1a68fb28f 100644 --- a/app/screens/interactive_dialog/index.js +++ b/app/screens/interactive_dialog/index.js @@ -4,8 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {submitInteractiveDialog} from '@mm-redux/actions/integrations'; import InteractiveDialog from './interactive_dialog'; diff --git a/app/screens/interactive_dialog/interactive_dialog.js b/app/screens/interactive_dialog/interactive_dialog.js index d384e33c8..3e16b5461 100644 --- a/app/screens/interactive_dialog/interactive_dialog.js +++ b/app/screens/interactive_dialog/interactive_dialog.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {Dimensions, ScrollView, View} from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {checkDialogElementForError, checkIfErrorsMatchElements} from 'mattermost-redux/utils/integration_utils'; +import {checkDialogElementForError, checkIfErrorsMatchElements} from '@mm-redux/utils/integration_utils'; import ErrorText from 'app/components/error_text'; import StatusBar from 'app/components/status_bar'; diff --git a/app/screens/interactive_dialog/interactive_dialog.test.js b/app/screens/interactive_dialog/interactive_dialog.test.js index dc243c1ce..66fd7b95d 100644 --- a/app/screens/interactive_dialog/interactive_dialog.test.js +++ b/app/screens/interactive_dialog/interactive_dialog.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import InteractiveDialog from './interactive_dialog'; diff --git a/app/screens/login/index.js b/app/screens/login/index.js index 280365ee4..490fe6f6c 100644 --- a/app/screens/login/index.js +++ b/app/screens/login/index.js @@ -5,8 +5,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {login} from 'app/actions/views/user'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; import {isLandscape} from 'app/selectors/device'; import LoginActions from 'app/actions/views/login'; diff --git a/app/screens/login_options/index.js b/app/screens/login_options/index.js index f42ee03b5..1a30ba32c 100644 --- a/app/screens/login_options/index.js +++ b/app/screens/login_options/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; import {isLandscape} from 'app/selectors/device'; import LoginOptions from './login_options'; diff --git a/app/screens/long_post/index.js b/app/screens/long_post/index.js index e80ffee2c..f858f3f79 100644 --- a/app/screens/long_post/index.js +++ b/app/screens/long_post/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectPost} from 'mattermost-redux/actions/posts'; -import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getPost} from 'mattermost-redux/selectors/entities/posts'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {selectPost} from '@mm-redux/actions/posts'; +import {makeGetChannel} from '@mm-redux/selectors/entities/channels'; +import {getPost} from '@mm-redux/selectors/entities/posts'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {loadThreadIfNecessary} from 'app/actions/views/channel'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/screens/long_post/long_post.test.js b/app/screens/long_post/long_post.test.js index c6786b14a..7d8baa239 100644 --- a/app/screens/long_post/long_post.test.js +++ b/app/screens/long_post/long_post.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/more_channels/index.js b/app/screens/more_channels/index.js index afb4d6f0d..712ae6dae 100644 --- a/app/screens/more_channels/index.js +++ b/app/screens/more_channels/index.js @@ -5,21 +5,21 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {createSelector} from 'reselect'; import {isLandscape} from 'app/selectors/device'; -import {General} from 'mattermost-redux/constants'; -import {getArchivedChannels, getChannels, joinChannel, searchChannels} from 'mattermost-redux/actions/channels'; -import {getChannelsInCurrentTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; -import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {General} from '@mm-redux/constants'; +import {getArchivedChannels, getChannels, joinChannel, searchChannels} from '@mm-redux/actions/channels'; +import {getChannelsInCurrentTeam, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId, getCurrentUserRoles} from '@mm-redux/selectors/entities/users'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {showCreateOption} from '@mm-redux/utils/channel_utils'; +import {isAdmin, isSystemAdmin} from '@mm-redux/utils/user_utils'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel'; import MoreChannels from './more_channels'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; const joinablePublicChannels = createSelector( getChannelsInCurrentTeam, diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js index e495f0916..08e8ca733 100644 --- a/app/screens/more_channels/more_channels.js +++ b/app/screens/more_channels/more_channels.js @@ -8,9 +8,9 @@ import {Platform, View, Text} from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import {Navigation} from 'react-native-navigation'; -import {debounce} from 'mattermost-redux/actions/helpers'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {debounce} from '@mm-redux/actions/helpers'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import BottomSheet from 'app/utils/bottom_sheet'; diff --git a/app/screens/more_channels/more_channels.test.js b/app/screens/more_channels/more_channels.test.js index 702b7a688..4195168a9 100644 --- a/app/screens/more_channels/more_channels.test.js +++ b/app/screens/more_channels/more_channels.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/screens/more_dms/index.js b/app/screens/more_dms/index.js index d1a7760d9..2a2ca02f6 100644 --- a/app/screens/more_dms/index.js +++ b/app/screens/more_dms/index.js @@ -7,12 +7,12 @@ import {connect} from 'react-redux'; import {setChannelDisplayName} from 'app/actions/views/channel'; import {makeDirectChannel, makeGroupChannel} from 'app/actions/views/more_dms'; import {isLandscape} from 'app/selectors/device'; -import {getProfiles, getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users'; -import {General} from 'mattermost-redux/constants'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users'; +import {getProfiles, getProfilesInTeam, searchProfiles} from '@mm-redux/actions/users'; +import {General} from '@mm-redux/constants'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId, getUsers} from '@mm-redux/selectors/entities/users'; import MoreDirectMessages from './more_dms'; diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js index 1989addfb..12379813d 100644 --- a/app/screens/more_dms/more_dms.js +++ b/app/screens/more_dms/more_dms.js @@ -7,11 +7,11 @@ import {intlShape} from 'react-intl'; import {Platform, View} from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {debounce} from 'mattermost-redux/actions/helpers'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {getGroupDisplayNameFromUserIds} from 'mattermost-redux/utils/channel_utils'; -import {displayUsername, filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils'; +import {debounce} from '@mm-redux/actions/helpers'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {getGroupDisplayNameFromUserIds} from '@mm-redux/utils/channel_utils'; +import {displayUsername, filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list'; diff --git a/app/screens/more_dms/selected_users/index.js b/app/screens/more_dms/selected_users/index.js index c4fc12f1d..3450b5c2e 100644 --- a/app/screens/more_dms/selected_users/index.js +++ b/app/screens/more_dms/selected_users/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getUsers} from 'mattermost-redux/selectors/entities/users'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getUsers} from '@mm-redux/selectors/entities/users'; import SelectedUsers from './selected_users'; diff --git a/app/screens/more_dms/selected_users/selected_user.js b/app/screens/more_dms/selected_users/selected_user.js index 64e19b6aa..d99acb0a7 100644 --- a/app/screens/more_dms/selected_users/selected_user.js +++ b/app/screens/more_dms/selected_users/selected_user.js @@ -12,7 +12,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; export default class SelectedUser extends React.PureComponent { static propTypes = { diff --git a/app/screens/more_dms/selected_users/selected_users.test.js b/app/screens/more_dms/selected_users/selected_users.test.js index 3be10a394..22a646661 100644 --- a/app/screens/more_dms/selected_users/selected_users.test.js +++ b/app/screens/more_dms/selected_users/selected_users.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import SelectedUsers from './selected_users.js'; diff --git a/app/screens/notification/index.js b/app/screens/notification/index.js index 7674cf74a..f651bbc3c 100644 --- a/app/screens/notification/index.js +++ b/app/screens/notification/index.js @@ -7,10 +7,10 @@ import {connect} from 'react-redux'; import {loadFromPushNotification} from 'app/actions/views/root'; import {getDimensions} from 'app/selectors/device'; -import {getChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getUser} from 'mattermost-redux/selectors/entities/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getChannel} from '@mm-redux/selectors/entities/channels'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getUser} from '@mm-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; import Notification from './notification'; diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js index 3e69e963d..3338e7594 100644 --- a/app/screens/notification/notification.js +++ b/app/screens/notification/notification.js @@ -16,10 +16,10 @@ import {Navigation} from 'react-native-navigation'; import * as Animatable from 'react-native-animatable'; import {PanGestureHandler} from 'react-native-gesture-handler'; -import {Client4} from 'mattermost-redux/client'; -import {isDirectChannel} from 'mattermost-redux/utils/channel_utils'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {Client4} from '@mm-redux/client'; +import {isDirectChannel} from '@mm-redux/utils/channel_utils'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import {popToRoot, dismissAllModals, dismissOverlay} from 'app/actions/navigation'; import FormattedText from 'app/components/formatted_text'; diff --git a/app/screens/notification/notification.test.js b/app/screens/notification/notification.test.js index b89a5c4c1..9026e2999 100644 --- a/app/screens/notification/notification.test.js +++ b/app/screens/notification/notification.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import Notification from './notification.js'; diff --git a/app/screens/options_modal/options_modal.js b/app/screens/options_modal/options_modal.js index e2e2f680d..cbb5646f0 100644 --- a/app/screens/options_modal/options_modal.js +++ b/app/screens/options_modal/options_modal.js @@ -11,7 +11,7 @@ import { View, } from 'react-native'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {NavigationTypes} from 'app/constants'; import {emptyFunction} from 'app/utils/general'; diff --git a/app/screens/permalink/index.js b/app/screens/permalink/index.js index 10a1eff44..fa6525372 100644 --- a/app/screens/permalink/index.js +++ b/app/screens/permalink/index.js @@ -4,13 +4,13 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getChannel as getChannelAction, joinChannel} from 'mattermost-redux/actions/channels'; -import {selectPost} from 'mattermost-redux/actions/posts'; -import {makeGetChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; -import {makeGetPostIdsAroundPost, getPost} from 'mattermost-redux/selectors/entities/posts'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getChannel as getChannelAction, joinChannel} from '@mm-redux/actions/channels'; +import {selectPost} from '@mm-redux/actions/posts'; +import {makeGetChannel, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; +import {makeGetPostIdsAroundPost, getPost} from '@mm-redux/selectors/entities/posts'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; import { handleSelectChannel, diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js index d1ce0eb75..5e4049f02 100644 --- a/app/screens/permalink/permalink.js +++ b/app/screens/permalink/permalink.js @@ -15,9 +15,9 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {Navigation} from 'react-native-navigation'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {getLastPostIndex} from 'mattermost-redux/utils/post_list'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {getLastPostIndex} from '@mm-redux/utils/post_list'; import FormattedText from 'app/components/formatted_text'; import Loading from 'app/components/loading'; diff --git a/app/screens/permalink/permalink.test.js b/app/screens/permalink/permalink.test.js index d104633b9..b9ab182b4 100644 --- a/app/screens/permalink/permalink.test.js +++ b/app/screens/permalink/permalink.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import Permalink from './permalink.js'; diff --git a/app/screens/pinned_posts/index.js b/app/screens/pinned_posts/index.js index b2accffbc..ebea713ec 100644 --- a/app/screens/pinned_posts/index.js +++ b/app/screens/pinned_posts/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts'; -import {clearSearch, getPinnedPosts} from 'mattermost-redux/actions/search'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {selectFocusedPostId, selectPost} from '@mm-redux/actions/posts'; +import {clearSearch, getPinnedPosts} from '@mm-redux/actions/search'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; diff --git a/app/screens/pinned_posts/pinned_posts.js b/app/screens/pinned_posts/pinned_posts.js index 1f6a997e2..d2b69aa31 100644 --- a/app/screens/pinned_posts/pinned_posts.js +++ b/app/screens/pinned_posts/pinned_posts.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {isDateLine, getDateForDateLine} from 'mattermost-redux/utils/post_list'; +import {isDateLine, getDateForDateLine} from '@mm-redux/utils/post_list'; import ChannelLoader from 'app/components/channel_loader'; import DateHeader from 'app/components/post_list/date_header'; diff --git a/app/screens/pinned_posts/pinned_posts.test.js b/app/screens/pinned_posts/pinned_posts.test.js index f7f889ba2..b2038f4e9 100644 --- a/app/screens/pinned_posts/pinned_posts.test.js +++ b/app/screens/pinned_posts/pinned_posts.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/post_options/index.js b/app/screens/post_options/index.js index c290e70ea..bc5fe0d1e 100644 --- a/app/screens/post_options/index.js +++ b/app/screens/post_options/index.js @@ -12,17 +12,17 @@ import { unpinPost, removePost, setUnreadPost, -} from 'mattermost-redux/actions/posts'; -import {General, Permissions} from 'mattermost-redux/constants'; -import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; -import {getChannel, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; -import {getCurrentTeamId, getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams'; -import {canEditPost} from 'mattermost-redux/utils/post_utils'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +} from '@mm-redux/actions/posts'; +import {General, Permissions} from '@mm-redux/constants'; +import {makeGetReactionsForPost} from '@mm-redux/selectors/entities/posts'; +import {getChannel, getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles'; +import {getCurrentTeamId, getCurrentTeamUrl} from '@mm-redux/selectors/entities/teams'; +import {canEditPost} from '@mm-redux/utils/post_utils'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {MAX_ALLOWED_REACTIONS} from 'app/constants/emoji'; import {THREAD} from 'app/constants/screen'; diff --git a/app/screens/post_options/index.test.js b/app/screens/post_options/index.test.js index 848753d06..6617c360c 100644 --- a/app/screens/post_options/index.test.js +++ b/app/screens/post_options/index.test.js @@ -2,16 +2,16 @@ // See LICENSE.txt for license information. import {makeMapStateToProps} from './index'; -import * as channelSelectors from 'mattermost-redux/selectors/entities/channels'; -import * as generalSelectors from 'mattermost-redux/selectors/entities/general'; -import * as userSelectors from 'mattermost-redux/selectors/entities/users'; -import * as commonSelectors from 'mattermost-redux/selectors/entities/common'; -import * as teamSelectors from 'mattermost-redux/selectors/entities/teams'; +import * as channelSelectors from '@mm-redux/selectors/entities/channels'; +import * as generalSelectors from '@mm-redux/selectors/entities/general'; +import * as userSelectors from '@mm-redux/selectors/entities/users'; +import * as commonSelectors from '@mm-redux/selectors/entities/common'; +import * as teamSelectors from '@mm-redux/selectors/entities/teams'; import * as deviceSelectors from 'app/selectors/device'; -import * as preferencesSelectors from 'mattermost-redux/selectors/entities/preferences'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import * as preferencesSelectors from '@mm-redux/selectors/entities/preferences'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; -jest.mock('mattermost-redux/utils/post_utils'); +jest.mock('@mm-redux/utils/post_utils'); channelSelectors.getChannel = jest.fn(); channelSelectors.getCurrentChannelId = jest.fn(); diff --git a/app/screens/post_options/post_options.js b/app/screens/post_options/post_options.js index cbdea8d33..f61472689 100644 --- a/app/screens/post_options/post_options.js +++ b/app/screens/post_options/post_options.js @@ -7,14 +7,14 @@ import {Alert, Clipboard, StyleSheet, View} from 'react-native'; import {intlShape} from 'react-intl'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import SlideUpPanel from 'app/components/slide_up_panel'; import {BOTTOM_MARGIN} from 'app/components/slide_up_panel/slide_up_panel'; import {t} from 'app/utils/i18n'; import {showModal, dismissModal} from 'app/actions/navigation'; -import {isSystemMessage} from 'mattermost-redux/utils/post_utils'; +import {isSystemMessage} from '@mm-redux/utils/post_utils'; import {OPTION_HEIGHT, getInitialPosition} from './post_options_utils'; import PostOption from './post_option'; diff --git a/app/screens/post_options/post_options.test.js b/app/screens/post_options/post_options.test.js index df4b1ea57..744b0b7c5 100644 --- a/app/screens/post_options/post_options.test.js +++ b/app/screens/post_options/post_options.test.js @@ -5,7 +5,7 @@ import React from 'react'; import {Alert} from 'react-native'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import PostOptions from './post_options'; diff --git a/app/screens/reaction_list/index.js b/app/screens/reaction_list/index.js index 337daf32f..a8d326ce4 100644 --- a/app/screens/reaction_list/index.js +++ b/app/screens/reaction_list/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getMissingProfilesByIds} from 'mattermost-redux/actions/users'; -import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentUserId, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users'; -import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getMissingProfilesByIds} from '@mm-redux/actions/users'; +import {makeGetReactionsForPost} from '@mm-redux/selectors/entities/posts'; +import {getCurrentUserId, makeGetProfilesByIdsAndUsernames} from '@mm-redux/selectors/entities/users'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import {getUniqueUserIds} from 'app/utils/reaction'; diff --git a/app/screens/reaction_list/reaction_header.test.js b/app/screens/reaction_list/reaction_header.test.js index b72c7732d..c50fecf75 100644 --- a/app/screens/reaction_list/reaction_header.test.js +++ b/app/screens/reaction_list/reaction_header.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import {ScrollView} from 'react-native'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ReactionHeader from './reaction_header'; diff --git a/app/screens/reaction_list/reaction_header_item.test.js b/app/screens/reaction_list/reaction_header_item.test.js index d15b565e0..ba86168e6 100644 --- a/app/screens/reaction_list/reaction_header_item.test.js +++ b/app/screens/reaction_list/reaction_header_item.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import {TouchableOpacity} from 'react-native'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ReactionHeaderItem from './reaction_header_item'; diff --git a/app/screens/reaction_list/reaction_list.test.js b/app/screens/reaction_list/reaction_list.test.js index d9c297302..75675f419 100644 --- a/app/screens/reaction_list/reaction_list.test.js +++ b/app/screens/reaction_list/reaction_list.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import SlideUpPanel from 'app/components/slide_up_panel'; diff --git a/app/screens/reaction_list/reaction_row/reaction_row.js b/app/screens/reaction_list/reaction_row/reaction_row.js index 458fa418c..285cd3cc6 100644 --- a/app/screens/reaction_list/reaction_row/reaction_row.js +++ b/app/screens/reaction_list/reaction_row/reaction_row.js @@ -10,7 +10,7 @@ import { View, } from 'react-native'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import ProfilePicture from 'app/components/profile_picture'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; diff --git a/app/screens/reaction_list/reaction_row/reaction_row.test.js b/app/screens/reaction_list/reaction_row/reaction_row.test.js index 7643c2e50..d8ccd9f84 100644 --- a/app/screens/reaction_list/reaction_row/reaction_row.test.js +++ b/app/screens/reaction_list/reaction_row/reaction_row.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import ReactionRow from './reaction_row'; diff --git a/app/screens/recent_mentions/index.js b/app/screens/recent_mentions/index.js index f9dd4b749..5b8311992 100644 --- a/app/screens/recent_mentions/index.js +++ b/app/screens/recent_mentions/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts'; -import {clearSearch, getRecentMentions} from 'mattermost-redux/actions/search'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {selectFocusedPostId, selectPost} from '@mm-redux/actions/posts'; +import {clearSearch, getRecentMentions} from '@mm-redux/actions/search'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; diff --git a/app/screens/recent_mentions/recent_mentions.js b/app/screens/recent_mentions/recent_mentions.js index 1ad43eab7..1a267ca95 100644 --- a/app/screens/recent_mentions/recent_mentions.js +++ b/app/screens/recent_mentions/recent_mentions.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {isDateLine, getDateForDateLine} from 'mattermost-redux/utils/post_list'; +import {isDateLine, getDateForDateLine} from '@mm-redux/utils/post_list'; import ChannelLoader from 'app/components/channel_loader'; import DateHeader from 'app/components/post_list/date_header'; diff --git a/app/screens/recent_mentions/recent_mentions.test.js b/app/screens/recent_mentions/recent_mentions.test.js index c85e222ca..1724a9610 100644 --- a/app/screens/recent_mentions/recent_mentions.test.js +++ b/app/screens/recent_mentions/recent_mentions.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/search/channel_display_name/index.js b/app/screens/search/channel_display_name/index.js index d62660a79..18de26c16 100644 --- a/app/screens/search/channel_display_name/index.js +++ b/app/screens/search/channel_display_name/index.js @@ -3,9 +3,9 @@ import {connect} from 'react-redux'; -import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getPost} from 'mattermost-redux/selectors/entities/posts'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {makeGetChannel} from '@mm-redux/selectors/entities/channels'; +import {getPost} from '@mm-redux/selectors/entities/posts'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import ChannelDisplayName from './channel_display_name'; diff --git a/app/screens/search/index.js b/app/screens/search/index.js index 9ab08383f..8ddbc13e5 100644 --- a/app/screens/search/index.js +++ b/app/screens/search/index.js @@ -4,16 +4,16 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts'; -import {clearSearch, removeSearchTerms, searchPostsWithParams, getMorePostsForSearch} from 'mattermost-redux/actions/search'; -import {getCurrentChannelId, filterPostIds} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {selectFocusedPostId, selectPost} from '@mm-redux/actions/posts'; +import {clearSearch, removeSearchTerms, searchPostsWithParams, getMorePostsForSearch} from '@mm-redux/actions/search'; +import {getCurrentChannelId, filterPostIds} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; import {handleSearchDraftChanged} from 'app/actions/views/search'; diff --git a/app/screens/search/modifier.test.js b/app/screens/search/modifier.test.js index b509371a2..38d2f5983 100644 --- a/app/screens/search/modifier.test.js +++ b/app/screens/search/modifier.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import Modifier from './modifier'; diff --git a/app/screens/search/recent_item.test.js b/app/screens/search/recent_item.test.js index 4721e1276..240d43f14 100644 --- a/app/screens/search/recent_item.test.js +++ b/app/screens/search/recent_item.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import RecentItem from './recent_item'; diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 76009b402..3e0ff3b26 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -14,8 +14,8 @@ import { import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {Navigation} from 'react-native-navigation'; -import {debounce} from 'mattermost-redux/actions/helpers'; -import {isDateLine, getDateForDateLine} from 'mattermost-redux/utils/post_list'; +import {debounce} from '@mm-redux/actions/helpers'; +import {isDateLine, getDateForDateLine} from '@mm-redux/utils/post_list'; import Autocomplete from 'app/components/autocomplete'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; diff --git a/app/screens/search/search_result_post/index.js b/app/screens/search/search_result_post/index.js index a438878e1..73582e5f1 100644 --- a/app/screens/search/search_result_post/index.js +++ b/app/screens/search/search_result_post/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {Posts} from 'mattermost-redux/constants'; -import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {Posts} from '@mm-redux/constants'; +import {getPost} from '@mm-redux/selectors/entities/posts'; import SearchResultPost from './search_result_post'; diff --git a/app/screens/select_server/index.js b/app/screens/select_server/index.js index 1f9195c4f..f42f242fd 100644 --- a/app/screens/select_server/index.js +++ b/app/screens/select_server/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getPing, resetPing, setServerVersion} from 'mattermost-redux/actions/general'; -import {login} from 'mattermost-redux/actions/users'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getPing, resetPing, setServerVersion} from '@mm-redux/actions/general'; +import {login} from '@mm-redux/actions/users'; +import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login'; diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index 82ac757c8..83932cb67 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -25,7 +25,7 @@ import RNFetchBlob from 'rn-fetch-blob'; import merge from 'deepmerge'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; diff --git a/app/screens/select_team/index.js b/app/screens/select_team/index.js index 3d5392ec3..582afa91e 100644 --- a/app/screens/select_team/index.js +++ b/app/screens/select_team/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTeams, addUserToTeam, joinTeam} from 'mattermost-redux/actions/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getTeams, addUserToTeam, joinTeam} from '@mm-redux/actions/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getJoinableTeams} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {logout} from 'app/actions/views/user'; import {handleTeamChange} from 'app/actions/views/select_team'; diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js index 983fd66a9..e438b2fb8 100644 --- a/app/screens/select_team/select_team.js +++ b/app/screens/select_team/select_team.js @@ -12,9 +12,9 @@ import { } from 'react-native'; import {Navigation} from 'react-native-navigation'; -import {RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {RequestStatus} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import FormattedText from 'app/components/formatted_text'; import Loading from 'app/components/loading'; diff --git a/app/screens/select_team/select_team.test.js b/app/screens/select_team/select_team.test.js index bd680f892..1e79347b1 100644 --- a/app/screens/select_team/select_team.test.js +++ b/app/screens/select_team/select_team.test.js @@ -3,9 +3,9 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; -import {RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from '@mm-redux/constants'; import SelectTeam from './select_team.js'; diff --git a/app/screens/selector_screen/index.js b/app/screens/selector_screen/index.js index 47c5f45f0..6a282e6a7 100644 --- a/app/screens/selector_screen/index.js +++ b/app/screens/selector_screen/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getProfiles, searchProfiles} from 'mattermost-redux/actions/users'; -import {getChannels, searchChannels} from 'mattermost-redux/actions/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getProfiles, searchProfiles} from '@mm-redux/actions/users'; +import {getChannels, searchChannels} from '@mm-redux/actions/channels'; import {isLandscape} from 'app/selectors/device'; import SelectorScreen from './selector_screen'; diff --git a/app/screens/selector_screen/selector_screen.js b/app/screens/selector_screen/selector_screen.js index f99ca17b6..869c36a28 100644 --- a/app/screens/selector_screen/selector_screen.js +++ b/app/screens/selector_screen/selector_screen.js @@ -9,11 +9,11 @@ import { View, } from 'react-native'; -import {debounce} from 'mattermost-redux/actions/helpers'; -import {General} from 'mattermost-redux/constants'; -import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils'; -import {filterChannelsMatchingTerm} from 'mattermost-redux/utils/channel_utils'; -import {memoizeResult} from 'mattermost-redux/utils/helpers'; +import {debounce} from '@mm-redux/actions/helpers'; +import {General} from '@mm-redux/constants'; +import {filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils'; +import {filterChannelsMatchingTerm} from '@mm-redux/utils/channel_utils'; +import {memoizeResult} from '@mm-redux/utils/helpers'; import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list'; import UserListRow from 'app/components/custom_list/user_list_row'; diff --git a/app/screens/selector_screen/selector_screen.test.js b/app/screens/selector_screen/selector_screen.test.js index bd15c32cc..15a7cac34 100644 --- a/app/screens/selector_screen/selector_screen.test.js +++ b/app/screens/selector_screen/selector_screen.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import {IntlProvider} from 'react-intl'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import SelectorScreen from './selector_screen.js'; diff --git a/app/screens/settings/advanced_settings/advanced_settings.js b/app/screens/settings/advanced_settings/advanced_settings.js index c90c969c4..c6ded2e5f 100644 --- a/app/screens/settings/advanced_settings/advanced_settings.js +++ b/app/screens/settings/advanced_settings/advanced_settings.js @@ -14,7 +14,7 @@ import { } from 'react-native'; import * as Sentry from '@sentry/react-native'; -import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils'; +import {getFormattedFileSize} from '@mm-redux/utils/file_utils'; import SettingsItem from 'app/screens/settings/settings_item'; import StatusBar from 'app/components/status_bar'; diff --git a/app/screens/settings/advanced_settings/index.js b/app/screens/settings/advanced_settings/index.js index 0655ea903..d65046800 100644 --- a/app/screens/settings/advanced_settings/index.js +++ b/app/screens/settings/advanced_settings/index.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {purgeOfflineStore} from 'app/actions/views/root'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import AdvancedSettings from './advanced_settings'; diff --git a/app/screens/settings/clock_display/clock_display_base.js b/app/screens/settings/clock_display/clock_display_base.js index 3c9386a08..1d0ffb170 100644 --- a/app/screens/settings/clock_display/clock_display_base.js +++ b/app/screens/settings/clock_display/clock_display_base.js @@ -5,7 +5,7 @@ import {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; export default class ClockDisplayBase extends PureComponent { static propTypes = { diff --git a/app/screens/settings/clock_display/index.js b/app/screens/settings/clock_display/index.js index 0ff012eac..7802ded45 100644 --- a/app/screens/settings/clock_display/index.js +++ b/app/screens/settings/clock_display/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme, get as getPreference} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import Preferences from 'mattermost-redux/constants/preferences'; +import {getTheme, get as getPreference} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {savePreferences} from '@mm-redux/actions/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {isLandscape} from 'app/selectors/device'; import ClockDisplay from './clock_display'; diff --git a/app/screens/settings/display_settings/display_settings.test.js b/app/screens/settings/display_settings/display_settings.test.js index a76dfbb59..b6670062c 100644 --- a/app/screens/settings/display_settings/display_settings.test.js +++ b/app/screens/settings/display_settings/display_settings.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {DeviceTypes} from 'app/constants'; import SettingsItem from 'app/screens/settings/settings_item'; diff --git a/app/screens/settings/display_settings/index.js b/app/screens/settings/display_settings/index.js index 4db4e9d1a..379a2e02d 100644 --- a/app/screens/settings/display_settings/index.js +++ b/app/screens/settings/display_settings/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; import {getAllowedThemes} from 'app/selectors/theme'; import {isThemeSwitchingEnabled} from 'app/utils/theme'; diff --git a/app/screens/settings/general/index.js b/app/screens/settings/general/index.js index 38efbe620..b0b71a9b2 100644 --- a/app/screens/settings/general/index.js +++ b/app/screens/settings/general/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {clearErrors} from 'mattermost-redux/actions/errors'; -import {getCurrentUrl, getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {clearErrors} from '@mm-redux/actions/errors'; +import {getCurrentUrl, getConfig} from '@mm-redux/selectors/entities/general'; +import {getJoinableTeams} from '@mm-redux/selectors/entities/teams'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {purgeOfflineStore} from 'app/actions/views/root'; import {isLandscape} from 'app/selectors/device'; diff --git a/app/screens/settings/notification_settings/index.js b/app/screens/settings/notification_settings/index.js index 7104f03b2..b5bdafb63 100644 --- a/app/screens/settings/notification_settings/index.js +++ b/app/screens/settings/notification_settings/index.js @@ -4,12 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {getCurrentUser, getStatusForUserId} from '@mm-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getMyPreferences, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {isLandscape} from 'app/selectors/device'; -import {updateMe} from 'mattermost-redux/actions/users'; +import {updateMe} from '@mm-redux/actions/users'; import NotificationSettings from './notification_settings'; diff --git a/app/screens/settings/notification_settings/notification_settings.js b/app/screens/settings/notification_settings/notification_settings.js index dcfc27f54..827ae423c 100644 --- a/app/screens/settings/notification_settings/notification_settings.js +++ b/app/screens/settings/notification_settings/notification_settings.js @@ -12,7 +12,7 @@ import { } from 'react-native'; import deepEqual from 'deep-equal'; -import {General, RequestStatus} from 'mattermost-redux/constants'; +import {General, RequestStatus} from '@mm-redux/constants'; import StatusBar from 'app/components/status_bar'; import NotificationPreferences from 'app/notification_preferences'; diff --git a/app/screens/settings/notification_settings/notification_settings.test.js b/app/screens/settings/notification_settings/notification_settings.test.js index e445316cc..1ab8b145a 100644 --- a/app/screens/settings/notification_settings/notification_settings.test.js +++ b/app/screens/settings/notification_settings/notification_settings.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/settings/notification_settings_auto_responder/index.js b/app/screens/settings/notification_settings_auto_responder/index.js index e8a7105ea..4753460e2 100644 --- a/app/screens/settings/notification_settings_auto_responder/index.js +++ b/app/screens/settings/notification_settings_auto_responder/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users'; import {isLandscape} from 'app/selectors/device'; import NotificationSettingsAutoResponder from './notification_settings_auto_responder'; diff --git a/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js b/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js index 7b9c61029..d23a1e748 100644 --- a/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js +++ b/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js @@ -9,7 +9,7 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; -import {General} from 'mattermost-redux/constants'; +import {General} from '@mm-redux/constants'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import FormattedText from 'app/components/formatted_text'; import StatusBar from 'app/components/status_bar'; diff --git a/app/screens/settings/notification_settings_email/index.js b/app/screens/settings/notification_settings_email/index.js index 76cdcca41..7b44b292a 100644 --- a/app/screens/settings/notification_settings_email/index.js +++ b/app/screens/settings/notification_settings_email/index.js @@ -4,17 +4,17 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {updateMe} from 'mattermost-redux/actions/users'; +import {savePreferences} from '@mm-redux/actions/preferences'; +import {updateMe} from '@mm-redux/actions/users'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; import { get as getPreference, getTheme, -} from 'mattermost-redux/selectors/entities/preferences'; +} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import {getNotificationProps} from 'app/utils/notify_props'; diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.android.js b/app/screens/settings/notification_settings_email/notification_settings_email.android.js index 82331cfb3..dda15017d 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.android.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.android.js @@ -10,7 +10,7 @@ import { View, } from 'react-native'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {t} from 'app/utils/i18n'; diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js b/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js index 86a0f5924..b5646b8ec 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.ios.js b/app/screens/settings/notification_settings_email/notification_settings_email.ios.js index 80983f92f..2c1de7aa7 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.ios.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.ios.js @@ -7,7 +7,7 @@ import { ScrollView, View, } from 'react-native'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import FormattedText from 'app/components/formatted_text'; import StatusBar from 'app/components/status_bar'; diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js b/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js index 7b89e91c3..de59478fe 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import SectionItem from 'app/screens/settings/section_item'; diff --git a/app/screens/settings/notification_settings_email/notification_settings_email_base.js b/app/screens/settings/notification_settings_email/notification_settings_email_base.js index 86b23b38b..f6686a510 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email_base.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email_base.js @@ -6,8 +6,8 @@ import {Platform} from 'react-native'; import PropTypes from 'prop-types'; import {Navigation} from 'react-native-navigation'; -import {Preferences} from 'mattermost-redux/constants'; -import {getEmailInterval} from 'mattermost-redux/utils/notify_props'; +import {Preferences} from '@mm-redux/constants'; +import {getEmailInterval} from '@mm-redux/utils/notify_props'; export default class NotificationSettingsEmailBase extends PureComponent { static propTypes = { diff --git a/app/screens/settings/notification_settings_mentions/index.js b/app/screens/settings/notification_settings_mentions/index.js index ca04f8c97..7215b7bb6 100644 --- a/app/screens/settings/notification_settings_mentions/index.js +++ b/app/screens/settings/notification_settings_mentions/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import NotificationSettingsMentions from './notification_settings_mentions'; diff --git a/app/screens/settings/notification_settings_mentions_keywords/index.js b/app/screens/settings/notification_settings_mentions_keywords/index.js index c052de8fe..13aeca0e5 100644 --- a/app/screens/settings/notification_settings_mentions_keywords/index.js +++ b/app/screens/settings/notification_settings_mentions_keywords/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import NotificationSettingsMentionsKeywords from './notification_settings_mentions_keywords'; diff --git a/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.test.js b/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.test.js index 224addfbe..79a5e0914 100644 --- a/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.test.js +++ b/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.test.js @@ -3,7 +3,7 @@ import React from 'react'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; diff --git a/app/screens/settings/notification_settings_mobile/index.js b/app/screens/settings/notification_settings_mobile/index.js index a3b0603fa..ce2e1f506 100644 --- a/app/screens/settings/notification_settings_mobile/index.js +++ b/app/screens/settings/notification_settings_mobile/index.js @@ -4,11 +4,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {isLandscape} from 'app/selectors/device'; -import {updateMe} from 'mattermost-redux/actions/users'; +import {updateMe} from '@mm-redux/actions/users'; import NotificationSettingsMobile from './notification_settings_mobile'; diff --git a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js index 0d6973d15..9c7ac7d44 100644 --- a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js @@ -16,7 +16,7 @@ import { import deepEqual from 'deep-equal'; import PropTypes from 'prop-types'; -import {RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from '@mm-redux/constants'; import FormattedText from 'app/components/formatted_text'; import RadioButtonGroup from 'app/components/radio_button'; diff --git a/app/screens/settings/sidebar/index.js b/app/screens/settings/sidebar/index.js index 1455b103a..0a23aeefb 100644 --- a/app/screens/settings/sidebar/index.js +++ b/app/screens/settings/sidebar/index.js @@ -10,7 +10,7 @@ import { import {intlShape} from 'react-intl'; import AsyncStorage from '@react-native-community/async-storage'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {DeviceTypes} from 'app/constants'; import FormattedText from 'app/components/formatted_text'; diff --git a/app/screens/settings/sidebar/sidebar.test.js b/app/screens/settings/sidebar/sidebar.test.js index 41dc82f42..eb567eb6d 100644 --- a/app/screens/settings/sidebar/sidebar.test.js +++ b/app/screens/settings/sidebar/sidebar.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import {DeviceTypes} from 'app/constants'; import MainSidebar from 'app/components/sidebars/main/main_sidebar.ios'; diff --git a/app/screens/settings/theme/index.js b/app/screens/settings/theme/index.js index cb353f8a0..ff8af39ec 100644 --- a/app/screens/settings/theme/index.js +++ b/app/screens/settings/theme/index.js @@ -4,10 +4,10 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {savePreferences} from '@mm-redux/actions/preferences'; import {getAllowedThemes, getCustomTheme} from 'app/selectors/theme'; import {isLandscape, isTablet} from 'app/selectors/device'; diff --git a/app/screens/settings/theme/theme.js b/app/screens/settings/theme/theme.js index 4944857e0..946af24b9 100644 --- a/app/screens/settings/theme/theme.js +++ b/app/screens/settings/theme/theme.js @@ -6,7 +6,7 @@ import {Text, View} from 'react-native'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import StatusBar from 'app/components/status_bar'; import Section from 'app/screens/settings/section'; diff --git a/app/screens/settings/theme/theme.test.js b/app/screens/settings/theme/theme.test.js index 4a28fc4c5..2256b54d1 100644 --- a/app/screens/settings/theme/theme.test.js +++ b/app/screens/settings/theme/theme.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import Theme from './theme'; import ThemeTile from './theme_tile'; diff --git a/app/screens/settings/timezone/index.js b/app/screens/settings/timezone/index.js index fa3799dd7..e8f21ed96 100644 --- a/app/screens/settings/timezone/index.js +++ b/app/screens/settings/timezone/index.js @@ -4,11 +4,11 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getSupportedTimezones} from 'mattermost-redux/actions/general'; -import {getSupportedTimezones as getTimezones} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getSupportedTimezones} from '@mm-redux/actions/general'; +import {getSupportedTimezones as getTimezones} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getUserTimezone} from '@mm-redux/selectors/entities/timezone'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {isLandscape} from 'app/selectors/device'; import {updateUser} from 'app/actions/views/edit_profile'; diff --git a/app/screens/settings/timezone/select_timezone/index.js b/app/screens/settings/timezone/select_timezone/index.js index 632b30363..117f51980 100644 --- a/app/screens/settings/timezone/select_timezone/index.js +++ b/app/screens/settings/timezone/select_timezone/index.js @@ -3,8 +3,8 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getSupportedTimezones} from 'mattermost-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getSupportedTimezones} from '@mm-redux/selectors/entities/general'; import {isLandscape} from 'app/selectors/device'; import SelectTimezone from './select_timezone'; diff --git a/app/screens/settings/timezone/select_timezone/select_timezone.js b/app/screens/settings/timezone/select_timezone/select_timezone.js index 829588c3b..371ac3f15 100644 --- a/app/screens/settings/timezone/select_timezone/select_timezone.js +++ b/app/screens/settings/timezone/select_timezone/select_timezone.js @@ -8,7 +8,7 @@ import { FlatList, Platform, } from 'react-native'; -import {getTimezoneRegion} from 'mattermost-redux/utils/timezone_utils'; +import {getTimezoneRegion} from '@mm-redux/utils/timezone_utils'; import {intlShape} from 'react-intl'; import SearchBar from 'app/components/search_bar'; diff --git a/app/screens/settings/timezone/timezone.js b/app/screens/settings/timezone/timezone.js index c837f3add..a839422a5 100644 --- a/app/screens/settings/timezone/timezone.js +++ b/app/screens/settings/timezone/timezone.js @@ -10,7 +10,7 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; -import {getTimezoneRegion} from 'mattermost-redux/utils/timezone_utils'; +import {getTimezoneRegion} from '@mm-redux/utils/timezone_utils'; import FormattedText from 'app/components/formatted_text'; import StatusBar from 'app/components/status_bar'; diff --git a/app/screens/sso/index.js b/app/screens/sso/index.js index 34354fed4..724380852 100644 --- a/app/screens/sso/index.js +++ b/app/screens/sso/index.js @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import {scheduleExpiredNotification} from 'app/actions/views/login'; import {ssoLogin} from 'app/actions/views/user'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {isLandscape} from 'app/selectors/device'; import SSO from './sso'; diff --git a/app/screens/sso/sso.js b/app/screens/sso/sso.js index 87fafc257..64cb6747c 100644 --- a/app/screens/sso/sso.js +++ b/app/screens/sso/sso.js @@ -13,7 +13,7 @@ import {WebView} from 'react-native-webview'; import CookieManager from 'react-native-cookies'; import urlParse from 'url-parse'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import {ViewTypes} from 'app/constants'; import Loading from 'app/components/loading'; diff --git a/app/screens/terms_of_service/index.js b/app/screens/terms_of_service/index.js index ec4c3ce8a..a9e8af002 100644 --- a/app/screens/terms_of_service/index.js +++ b/app/screens/terms_of_service/index.js @@ -4,9 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTermsOfService, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTermsOfService, updateMyTermsOfServiceStatus} from '@mm-redux/actions/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {logout} from 'app/actions/views/user'; import TermsOfService from './terms_of_service.js'; diff --git a/app/screens/terms_of_service/terms_of_service.test.js b/app/screens/terms_of_service/terms_of_service.test.js index 4e95528e8..c7e580c16 100644 --- a/app/screens/terms_of_service/terms_of_service.test.js +++ b/app/screens/terms_of_service/terms_of_service.test.js @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/screens/text_preview/index.js b/app/screens/text_preview/index.js index d67cc359b..d1c86bfe4 100644 --- a/app/screens/text_preview/index.js +++ b/app/screens/text_preview/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; import TextPreview from './text_preview'; diff --git a/app/screens/thread/index.js b/app/screens/thread/index.js index 7f1573ab0..5aa499a26 100644 --- a/app/screens/thread/index.js +++ b/app/screens/thread/index.js @@ -4,11 +4,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; -import {selectPost} from 'mattermost-redux/actions/posts'; -import {makeGetChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; -import {makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts'; +import {selectPost} from '@mm-redux/actions/posts'; +import {makeGetChannel, getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/channels'; +import {makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts'; import Thread from './thread'; diff --git a/app/screens/thread/thread.ios.js b/app/screens/thread/thread.ios.js index 3ca40fb6e..f326c1e6f 100644 --- a/app/screens/thread/thread.ios.js +++ b/app/screens/thread/thread.ios.js @@ -5,7 +5,7 @@ import React from 'react'; import {View} from 'react-native'; import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view'; -import {getLastPostIndex} from 'mattermost-redux/utils/post_list'; +import {getLastPostIndex} from '@mm-redux/utils/post_list'; import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete'; import Loading from 'app/components/loading'; diff --git a/app/screens/thread/thread.ios.test.js b/app/screens/thread/thread.ios.test.js index b15ab6f55..62bb46e1d 100644 --- a/app/screens/thread/thread.ios.test.js +++ b/app/screens/thread/thread.ios.test.js @@ -4,8 +4,8 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; -import {General, RequestStatus} from 'mattermost-redux/constants'; +import Preferences from '@mm-redux/constants/preferences'; +import {General, RequestStatus} from '@mm-redux/constants'; import PostList from 'app/components/post_list'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/screens/thread/thread_base.js b/app/screens/thread/thread_base.js index c0c21fb91..9b0526aa3 100644 --- a/app/screens/thread/thread_base.js +++ b/app/screens/thread/thread_base.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {Keyboard} from 'react-native'; import {intlShape} from 'react-intl'; -import {General, RequestStatus} from 'mattermost-redux/constants'; +import {General, RequestStatus} from '@mm-redux/constants'; import Loading from 'app/components/loading'; import DeletedPost from 'app/components/deleted_post'; diff --git a/app/screens/user_profile/index.js b/app/screens/user_profile/index.js index e6383f723..5ca2bcb29 100644 --- a/app/screens/user_profile/index.js +++ b/app/screens/user_profile/index.js @@ -7,13 +7,13 @@ import {connect} from 'react-redux'; import {setChannelDisplayName} from 'app/actions/views/channel'; import {makeDirectChannel} from 'app/actions/views/more_dms'; import {isLandscape} from 'app/selectors/device'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTeammateNameDisplaySetting, getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import Preferences from 'mattermost-redux/constants/preferences'; -import {loadBot} from 'mattermost-redux/actions/bots'; -import {getBotAccounts} from 'mattermost-redux/selectors/entities/bots'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTeammateNameDisplaySetting, getTheme, getBool} from '@mm-redux/selectors/entities/preferences'; +import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; +import Preferences from '@mm-redux/constants/preferences'; +import {loadBot} from '@mm-redux/actions/bots'; +import {getBotAccounts} from '@mm-redux/selectors/entities/bots'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; import UserProfile from './user_profile'; diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js index 4412c99c8..516a990b5 100644 --- a/app/screens/user_profile/user_profile.js +++ b/app/screens/user_profile/user_profile.js @@ -12,8 +12,8 @@ import { import {intlShape} from 'react-intl'; import {Navigation} from 'react-native-navigation'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; +import {displayUsername} from '@mm-redux/utils/user_utils'; +import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import ProfilePicture from 'app/components/profile_picture'; diff --git a/app/screens/user_profile/user_profile.test.js b/app/screens/user_profile/user_profile.test.js index 5fc388122..f3c8bb12b 100644 --- a/app/screens/user_profile/user_profile.test.js +++ b/app/screens/user_profile/user_profile.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import Preferences from 'mattermost-redux/constants/preferences'; +import Preferences from '@mm-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; diff --git a/app/selectors/autocomplete.js b/app/selectors/autocomplete.js index 444559f3a..8f60fe7c3 100644 --- a/app/selectors/autocomplete.js +++ b/app/selectors/autocomplete.js @@ -3,15 +3,15 @@ import {createSelector} from 'reselect'; -import {General} from 'mattermost-redux/constants'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels'; +import {General} from '@mm-redux/constants'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getMyChannels, getOtherChannels} from '@mm-redux/selectors/entities/channels'; import { getCurrentUser, getProfilesInCurrentChannel, getProfilesNotInCurrentChannel, getProfilesInCurrentTeam, -} from 'mattermost-redux/selectors/entities/users'; -import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils'; -import {sortByUsername} from 'mattermost-redux/utils/user_utils'; +} from '@mm-redux/selectors/entities/users'; +import {sortChannelsByDisplayName} from '@mm-redux/utils/channel_utils'; +import {sortByUsername} from '@mm-redux/utils/user_utils'; import * as Autocomplete from 'app/constants/autocomplete'; import {getCurrentLocale} from 'app/selectors/i18n'; diff --git a/app/selectors/channel.js b/app/selectors/channel.js index 1b26694ef..b37697d50 100644 --- a/app/selectors/channel.js +++ b/app/selectors/channel.js @@ -3,9 +3,9 @@ import {createSelector} from 'reselect'; -import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; -import {getChannelByName} from 'mattermost-redux/selectors/entities/channels'; -import {getTeamByName} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; +import {getChannelByName} from '@mm-redux/selectors/entities/channels'; +import {getTeamByName} from '@mm-redux/selectors/entities/teams'; const getOtherUserIdForDm = createSelector( (state, channel) => channel, diff --git a/app/selectors/client_upgrade.js b/app/selectors/client_upgrade.js index fce5696f6..bbd39620d 100644 --- a/app/selectors/client_upgrade.js +++ b/app/selectors/client_upgrade.js @@ -4,7 +4,7 @@ import {createSelector} from 'reselect'; import {Platform} from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getConfig} from '@mm-redux/selectors/entities/general'; import LocalConfig from 'assets/config'; diff --git a/app/selectors/i18n.js b/app/selectors/i18n.js index 603851a57..d4aebcdb7 100644 --- a/app/selectors/i18n.js +++ b/app/selectors/i18n.js @@ -3,8 +3,8 @@ import DeviceInfo from 'react-native-device-info'; -import {DEFAULT_LOCALE} from 'mattermost-redux/constants/general'; -import {getCurrentUserLocale} from 'mattermost-redux/selectors/entities/i18n'; +import {DEFAULT_LOCALE} from '@mm-redux/constants/general'; +import {getCurrentUserLocale} from '@mm-redux/selectors/entities/i18n'; // Not a proper selector since the device locale isn't in the redux store export function getCurrentLocale(state) { diff --git a/app/selectors/post_list.js b/app/selectors/post_list.js index 2e131a97b..f1527df97 100644 --- a/app/selectors/post_list.js +++ b/app/selectors/post_list.js @@ -1,12 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Posts} from 'mattermost-redux/constants'; -import {makeGetPostsForIds} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {createIdsSelector} from 'mattermost-redux/utils/helpers'; +import {Posts} from '@mm-redux/constants'; +import {makeGetPostsForIds} from '@mm-redux/selectors/entities/posts'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {createIdsSelector} from '@mm-redux/utils/helpers'; -import {DATE_LINE} from 'mattermost-redux/utils/post_list'; +import {DATE_LINE} from '@mm-redux/utils/post_list'; export {DATE_LINE}; export const DATE_LINE_SUFFIX = '-index-'; diff --git a/app/selectors/theme.js b/app/selectors/theme.js index 9198e4908..bb5125df9 100644 --- a/app/selectors/theme.js +++ b/app/selectors/theme.js @@ -3,9 +3,9 @@ import {createSelector} from 'reselect'; -import Preferences from 'mattermost-redux/constants/preferences'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import Preferences from '@mm-redux/constants/preferences'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getTheme} from '@mm-redux/selectors/entities/preferences'; export const getAllowedThemes = createSelector( getConfig, diff --git a/app/selectors/views.js b/app/selectors/views.js index f248d6883..13e393446 100644 --- a/app/selectors/views.js +++ b/app/selectors/views.js @@ -3,7 +3,7 @@ import {createSelector} from 'reselect'; -import {getCurrentChannelId, getUnreads} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentChannelId, getUnreads} from '@mm-redux/selectors/entities/channels'; const emptyDraft = { draft: '', diff --git a/app/store/store.js b/app/store/store.js index a555c5fb4..60f6b719d 100644 --- a/app/store/store.js +++ b/app/store/store.js @@ -7,11 +7,11 @@ import AsyncStorage from '@react-native-community/async-storage'; import {createBlacklistFilter} from 'redux-persist-transform-filter'; import {createTransform, persistStore} from 'redux-persist'; -import {ErrorTypes, GeneralTypes} from 'mattermost-redux/action_types'; -import {General} from 'mattermost-redux/constants'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import configureStore from 'mattermost-redux/store'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {ErrorTypes, GeneralTypes} from '@mm-redux/action_types'; +import {General} from '@mm-redux/constants'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import configureStore from '@mm-redux/store'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {NavigationTypes, ViewTypes} from 'app/constants'; import appReducer from 'app/reducers'; diff --git a/app/utils/channels.js b/app/utils/channels.js index 68a3cbfa1..53ae06dea 100644 --- a/app/utils/channels.js +++ b/app/utils/channels.js @@ -1,8 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Preferences} from 'mattermost-redux/constants'; -import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils'; +import {Preferences} from '@mm-redux/constants'; +import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils'; export function isDirectChannelVisible(userId, myPreferences, channel) { const channelId = getUserIdFromChannelName(userId, channel.name); diff --git a/app/utils/error_handling.js b/app/utils/error_handling.js index 69db424dc..dbbe5786b 100644 --- a/app/utils/error_handling.js +++ b/app/utils/error_handling.js @@ -9,8 +9,8 @@ import { setNativeExceptionHandler, } from 'react-native-exception-handler'; -import {Client4} from 'mattermost-redux/client'; -import {logError} from 'mattermost-redux/actions/errors'; +import {Client4} from '@mm-redux/client'; +import {logError} from '@mm-redux/actions/errors'; import {close as closeWebSocket} from '@actions/websocket'; import {purgeOfflineStore} from 'app/actions/views/root'; diff --git a/app/utils/file.js b/app/utils/file.js index 52247ba08..6756a78fe 100644 --- a/app/utils/file.js +++ b/app/utils/file.js @@ -5,7 +5,7 @@ import {Platform} from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; import mimeDB from 'mime-db'; -import {lookupMimeType} from 'mattermost-redux/utils/file_utils'; +import {lookupMimeType} from '@mm-redux/utils/file_utils'; const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i; diff --git a/app/utils/general.js b/app/utils/general.js index 8982dbb34..3025eb23e 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -4,7 +4,7 @@ import {Alert} from 'react-native'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; -import {Posts} from 'mattermost-redux/constants'; +import {Posts} from '@mm-redux/constants'; import {logout} from 'app/actions/views/user'; import EphemeralStore from 'app/store/ephemeral_store'; diff --git a/app/utils/image_cache_manager.js b/app/utils/image_cache_manager.js index 0cd161c7a..4ef9c4c54 100644 --- a/app/utils/image_cache_manager.js +++ b/app/utils/image_cache_manager.js @@ -8,7 +8,7 @@ import RNFetchBlob from 'rn-fetch-blob'; import Url from 'url-parse'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import {DeviceTypes} from 'app/constants'; import { diff --git a/app/utils/network.js b/app/utils/network.js index 784a9540d..6c65e34a7 100644 --- a/app/utils/network.js +++ b/app/utils/network.js @@ -5,7 +5,7 @@ import {Platform} from 'react-native'; import NetInfo from '@react-native-community/netinfo'; import RNFetchBlob from 'rn-fetch-blob'; -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import mattermostBucket from 'app/mattermost_bucket'; import mattermostManaged from 'app/mattermost_managed'; diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index 0455eac74..14db9d111 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -4,10 +4,10 @@ import {Platform} from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import {setDeviceToken} from 'mattermost-redux/actions/general'; -import {Client4} from 'mattermost-redux/client'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {setDeviceToken} from '@mm-redux/actions/general'; +import {Client4} from '@mm-redux/client'; +import {General} from '@mm-redux/constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {markChannelViewedAndRead, retryGetPostsAction} from 'app/actions/views/channel'; import {dismissAllModals, popToRoot} from 'app/actions/navigation'; diff --git a/app/utils/security.js b/app/utils/security.js index 40946f5ef..9295bef78 100644 --- a/app/utils/security.js +++ b/app/utils/security.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import CookieManager from 'react-native-cookies'; let mfaPreflightDone = false; diff --git a/app/utils/sentry/index.js b/app/utils/sentry/index.js index 55bb04985..0086d5b52 100644 --- a/app/utils/sentry/index.js +++ b/app/utils/sentry/index.js @@ -4,11 +4,11 @@ import {Platform} from 'react-native'; import Config from 'assets/config'; -import {Client4} from 'mattermost-redux/client'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeam, getCurrentTeamMembership} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; +import {Client4} from '@mm-redux/client'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getCurrentUser} from '@mm-redux/selectors/entities/users'; +import {getCurrentTeam, getCurrentTeamMembership} from '@mm-redux/selectors/entities/teams'; +import {getCurrentChannel, getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/channels'; export const LOGGER_EXTENSION = 'extension'; export const LOGGER_JAVASCRIPT = 'javascript'; diff --git a/app/utils/sentry/index.test.js b/app/utils/sentry/index.test.js index f0f64ba38..fd99000b6 100644 --- a/app/utils/sentry/index.test.js +++ b/app/utils/sentry/index.test.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; +import {Client4} from '@mm-redux/client'; import {cleanUrlForLogging} from 'app/utils/sentry'; diff --git a/app/utils/theme.js b/app/utils/theme.js index f26d45da4..8c3a4d9a9 100644 --- a/app/utils/theme.js +++ b/app/utils/theme.js @@ -4,7 +4,7 @@ import {StyleSheet} from 'react-native'; import tinyColor from 'tinycolor2'; -import * as ThemeUtils from 'mattermost-redux/utils/theme_utils'; +import * as ThemeUtils from '@mm-redux/utils/theme_utils'; import {mergeNavigationOptions} from 'app/actions/navigation'; diff --git a/app/utils/url.js b/app/utils/url.js index 41fdae19f..ffe5a4e16 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -4,7 +4,7 @@ import {latinise} from './latinise.js'; import {escapeRegex} from './markdown'; -import {Files} from 'mattermost-redux/constants'; +import {Files} from '@mm-redux/constants'; import {DeepLinkTypes} from 'app/constants'; diff --git a/babel.config.js b/babel.config.js index d409dce8c..c72284af0 100644 --- a/babel.config.js +++ b/babel.config.js @@ -19,6 +19,7 @@ module.exports = { '@telemetry': './app/telemetry', '@utils': './app/utils', '@websocket': './app/client/websocket', + '@mm-redux': './app/mm-redux', }, }], ], diff --git a/package-lock.json b/package-lock.json index 0a14d08b7..db9dc648b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,11 +30,11 @@ } }, "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.8.3" } }, "@babel/compat-data": { @@ -60,6 +60,7 @@ "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", + "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.8.7", @@ -78,122 +79,20 @@ "source-map": "^0.5.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - } - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, "@babel/generator": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.4.tgz", - "integrity": "sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==", + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", + "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", "requires": { - "@babel/types": "^7.7.4", + "@babel/types": "^7.9.5", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -205,23 +104,6 @@ "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", "requires": { "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-builder-binary-assignment-operator-visitor": { @@ -231,23 +113,6 @@ "requires": { "@babel/helper-explode-assignable-expression": "^7.8.3", "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-builder-react-jsx": { @@ -257,23 +122,6 @@ "requires": { "@babel/helper-annotate-as-pure": "^7.8.3", "@babel/types": "^7.9.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-builder-react-jsx-experimental": { @@ -284,23 +132,6 @@ "@babel/helper-annotate-as-pure": "^7.8.3", "@babel/helper-module-imports": "^7.8.3", "@babel/types": "^7.9.5" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-compilation-targets": { @@ -335,87 +166,6 @@ "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-replace-supers": "^7.8.6", "@babel/helper-split-export-declaration": "^7.8.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-create-regexp-features-plugin": { @@ -436,74 +186,6 @@ "@babel/helper-function-name": "^7.8.3", "@babel/types": "^7.8.3", "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-explode-assignable-expression": { @@ -513,129 +195,24 @@ "requires": { "@babel/traverse": "^7.8.3", "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - } - } - } } }, "@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", + "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.9.5" } }, "@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", "requires": { - "@babel/types": "^7.7.4" + "@babel/types": "^7.8.3" } }, "@babel/helper-hoist-variables": { @@ -645,25 +222,6 @@ "dev": true, "requires": { "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", - "dev": true - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-member-expression-to-functions": { @@ -672,23 +230,6 @@ "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", "requires": { "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-module-imports": { @@ -697,23 +238,6 @@ "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", "requires": { "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-module-transforms": { @@ -728,64 +252,6 @@ "@babel/template": "^7.8.6", "@babel/types": "^7.9.0", "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-optimise-call-expression": { @@ -794,30 +260,12 @@ "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", "requires": { "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" }, "@babel/helper-regex": { "version": "7.8.3", @@ -837,111 +285,6 @@ "@babel/template": "^7.8.3", "@babel/traverse": "^7.8.3", "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - } - } - } } }, "@babel/helper-replace-supers": { @@ -953,111 +296,6 @@ "@babel/helper-optimise-call-expression": "^7.8.3", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.6" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - } - } - } } }, "@babel/helper-simple-access": { @@ -1067,72 +305,20 @@ "requires": { "@babel/template": "^7.8.3", "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - } - } - } } }, "@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", "requires": { - "@babel/types": "^7.7.4" + "@babel/types": "^7.8.3" } }, "@babel/helper-validator-identifier": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==" + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" }, "@babel/helper-wrap-function": { "version": "7.8.3", @@ -1143,109 +329,6 @@ "@babel/template": "^7.8.3", "@babel/traverse": "^7.8.3", "@babel/types": "^7.8.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helpers": { @@ -1256,127 +339,22 @@ "@babel/template": "^7.8.3", "@babel/traverse": "^7.9.0", "@babel/types": "^7.9.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - } - } - } } }, "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", "requires": { + "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz", - "integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==" + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", + "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" }, "@babel/plugin-external-helpers": { "version": "7.8.3", @@ -1384,13 +362,6 @@ "integrity": "sha512-mx0WXDDiIl5DwzMtzWGRSPugXi9BxROS05GQrhLNbEamhBiicgn994ibwkyiBH+6png7bm/yA7AUsvHyCXi4Vw==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-async-generator-functions": { @@ -1402,14 +373,6 @@ "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-remap-async-to-generator": "^7.8.3", "@babel/plugin-syntax-async-generators": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-proposal-class-properties": { @@ -1419,13 +382,6 @@ "requires": { "@babel/helper-create-class-features-plugin": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-dynamic-import": { @@ -1436,14 +392,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-proposal-export-default-from": { @@ -1453,13 +401,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-json-strings": { @@ -1470,14 +411,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-proposal-nullish-coalescing-operator": { @@ -1487,13 +420,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-object-rest-spread": { @@ -1504,13 +430,6 @@ "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-transform-parameters": "^7.9.5" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-optional-catch-binding": { @@ -1520,13 +439,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-optional-chaining": { @@ -1536,13 +448,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-proposal-unicode-property-regex": { @@ -1553,14 +458,6 @@ "requires": { "@babel/helper-create-regexp-features-plugin": "^7.8.8", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-async-generators": { @@ -1570,14 +467,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-bigint": { @@ -1587,14 +476,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-class-properties": { @@ -1603,13 +484,6 @@ "integrity": "sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-dynamic-import": { @@ -1618,13 +492,6 @@ "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-export-default-from": { @@ -1633,13 +500,6 @@ "integrity": "sha512-a1qnnsr73KLNIQcQlcQ4ZHxqqfBKM6iNQZW2OMTyxNbA2WC7SHWHtGVpFzWtQAuS2pspkWVzdEBXXx8Ik0Za4w==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-flow": { @@ -1648,13 +508,6 @@ "integrity": "sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-json-strings": { @@ -1664,14 +517,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-jsx": { @@ -1680,13 +525,6 @@ "integrity": "sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-logical-assignment-operators": { @@ -1696,14 +534,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -1712,13 +542,6 @@ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-numeric-separator": { @@ -1728,14 +551,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-object-rest-spread": { @@ -1744,13 +559,6 @@ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-optional-catch-binding": { @@ -1759,13 +567,6 @@ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-optional-chaining": { @@ -1774,13 +575,6 @@ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-syntax-top-level-await": { @@ -1790,14 +584,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-syntax-typescript": { @@ -1806,13 +592,6 @@ "integrity": "sha512-GO1MQ/SGGGoiEXY0e0bSpHimJvxqB7lktLLIq2pv8xG7WZ8IMEle74jIe1FhprHBWjwjZtXHkycDLZXIWM5Wfg==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-arrow-functions": { @@ -1821,13 +600,6 @@ "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-async-to-generator": { @@ -1838,13 +610,6 @@ "@babel/helper-module-imports": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-remap-async-to-generator": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-block-scoped-functions": { @@ -1853,13 +618,6 @@ "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-block-scoping": { @@ -1869,13 +627,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-classes": { @@ -1891,87 +642,6 @@ "@babel/helper-replace-supers": "^7.8.6", "@babel/helper-split-export-declaration": "^7.8.3", "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-computed-properties": { @@ -1980,13 +650,6 @@ "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-destructuring": { @@ -1995,13 +658,6 @@ "integrity": "sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-dotall-regex": { @@ -2012,14 +668,6 @@ "requires": { "@babel/helper-create-regexp-features-plugin": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-duplicate-keys": { @@ -2029,14 +677,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-exponentiation-operator": { @@ -2046,13 +686,6 @@ "requires": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-flow-strip-types": { @@ -2062,13 +695,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-flow": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-for-of": { @@ -2077,13 +703,6 @@ "integrity": "sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-function-name": { @@ -2093,79 +712,6 @@ "requires": { "@babel/helper-function-name": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-literals": { @@ -2174,13 +720,6 @@ "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-member-expression-literals": { @@ -2189,13 +728,6 @@ "integrity": "sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-modules-amd": { @@ -2207,14 +739,6 @@ "@babel/helper-module-transforms": "^7.9.0", "@babel/helper-plugin-utils": "^7.8.3", "babel-plugin-dynamic-import-node": "^2.3.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-modules-commonjs": { @@ -2226,13 +750,6 @@ "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-simple-access": "^7.8.3", "babel-plugin-dynamic-import-node": "^2.3.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-modules-systemjs": { @@ -2245,14 +762,6 @@ "@babel/helper-module-transforms": "^7.9.0", "@babel/helper-plugin-utils": "^7.8.3", "babel-plugin-dynamic-import-node": "^2.3.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-modules-umd": { @@ -2263,14 +772,6 @@ "requires": { "@babel/helper-module-transforms": "^7.9.0", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-named-capturing-groups-regex": { @@ -2289,14 +790,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-object-assign": { @@ -2305,13 +798,6 @@ "integrity": "sha512-i3LuN8tPDqUCRFu3dkzF2r1Nx0jp4scxtm7JxtIqI9he9Vk20YD+/zshdzR9JLsoBMlJlNR82a62vQExNEVx/Q==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-object-super": { @@ -2321,13 +807,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-replace-supers": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-parameters": { @@ -2337,36 +816,6 @@ "requires": { "@babel/helper-get-function-arity": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-property-literals": { @@ -2375,13 +824,6 @@ "integrity": "sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-react-display-name": { @@ -2390,13 +832,6 @@ "integrity": "sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-react-jsx": { @@ -2408,13 +843,6 @@ "@babel/helper-builder-react-jsx-experimental": "^7.9.0", "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-jsx": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-react-jsx-source": { @@ -2424,13 +852,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-jsx": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-regenerator": { @@ -2448,14 +869,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-runtime": { @@ -2470,12 +883,6 @@ "semver": "^5.5.1" }, "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2490,13 +897,6 @@ "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-spread": { @@ -2505,13 +905,6 @@ "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==", "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-sticky-regex": { @@ -2521,13 +914,6 @@ "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-regex": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-template-literals": { @@ -2537,13 +923,6 @@ "requires": { "@babel/helper-annotate-as-pure": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-typeof-symbol": { @@ -2553,14 +932,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - } } }, "@babel/plugin-transform-typescript": { @@ -2571,13 +942,6 @@ "@babel/helper-create-class-features-plugin": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-typescript": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/plugin-transform-unicode-regex": { @@ -2587,13 +951,6 @@ "requires": { "@babel/helper-create-regexp-features-plugin": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" - } } }, "@babel/preset-env": { @@ -2661,29 +1018,6 @@ "semver": "^5.5.0" }, "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", - "dev": true - }, - "@babel/types": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", - "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2714,65 +1048,47 @@ } }, "@babel/runtime-corejs3": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.7.tgz", - "integrity": "sha512-sc7A+H4I8kTd7S61dgB9RomXu/C+F4IrRr4Ytze4dnfx7AXEpCrejSNpjx7vq6y/Bak9S6Kbk65a/WgMLtg43Q==", + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz", + "integrity": "sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA==", "dev": true, "requires": { "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", - "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==", - "dev": true - } } }, "@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" } }, "@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", + "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.5", + "@babel/helper-function-name": "^7.9.5", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.0", + "@babel/types": "^7.9.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } } }, "@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", + "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", "requires": { - "esutils": "^2.0.2", + "@babel/helper-validator-identifier": "^7.9.5", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } @@ -2784,19 +1100,12 @@ "dev": true }, "@cnakazawa/watch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", - "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", "requires": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } } }, "@egjs/hammerjs": { @@ -2953,12 +1262,6 @@ "strip-ansi": "^6.0.0" }, "dependencies": { - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, "@jest/console": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.3.0.tgz", @@ -2994,30 +1297,6 @@ "collect-v8-coverage": "^1.0.0" } }, - "@jest/transform": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.3.0.tgz", - "integrity": "sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.3.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.3.0", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.3.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, "@jest/types": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", @@ -3074,19 +1353,6 @@ "picomatch": "^2.0.4" } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -3161,27 +1427,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, "jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", @@ -3223,12 +1468,6 @@ "stack-utils": "^1.0.1" } }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, "jest-serializer": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", @@ -3314,12 +1553,6 @@ "react-is": "^16.12.0" } }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3365,17 +1598,6 @@ "has-flag": "^4.0.0" } }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3399,31 +1621,204 @@ "requires": { "isexe": "^2.0.0" } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } } } }, "@jest/environment": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", - "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.3.0.tgz", + "integrity": "sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==", "dev": true, "requires": { - "@jest/fake-timers": "^24.9.0", - "@jest/transform": "^24.9.0", - "@jest/types": "^24.9.0", - "jest-mock": "^24.9.0" + "@jest/fake-timers": "^25.3.0", + "@jest/types": "^25.3.0", + "jest-mock": "^25.3.0" + }, + "dependencies": { + "@jest/fake-timers": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", + "integrity": "sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0", + "jest-message-util": "^25.3.0", + "jest-mock": "^25.3.0", + "jest-util": "^25.3.0", + "lolex": "^5.0.0" + } + }, + "@jest/types": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", + "integrity": "sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-message-util": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.3.0.tgz", + "integrity": "sha512-5QNy9Id4WxJbRITEbA1T1kem9bk7y2fD0updZMSTNHtbEDnYOGLDPAuFBhFgVmOZpv0n6OMdVkK+WhyXEPCcOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.3.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.3.0.tgz", + "integrity": "sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0" + } + }, + "jest-util": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.3.0.tgz", + "integrity": "sha512-dc625P/KS/CpWTJJJxKc4bA3A6c+PJGBAqS8JTJqx4HqPoKNqXg/Ec8biL2Z1TabwK7E7Ilf0/ukSEXM1VwzNA==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0", + "chalk": "^3.0.0", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + } + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } } }, "@jest/fake-timers": { @@ -3468,12 +1863,6 @@ "v8-to-istanbul": "^4.0.1" }, "dependencies": { - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, "@jest/console": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.3.0.tgz", @@ -3509,30 +1898,6 @@ "collect-v8-coverage": "^1.0.0" } }, - "@jest/transform": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.3.0.tgz", - "integrity": "sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.3.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.3.0", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.3.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, "@jest/types": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", @@ -3574,19 +1939,6 @@ "picomatch": "^2.0.4" } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -3655,26 +2007,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "is-wsl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", + "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } + "optional": true }, "jest-haste-map": { "version": "25.3.0", @@ -3696,12 +2034,6 @@ "which": "^2.0.2" } }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, "jest-serializer": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", @@ -3755,18 +2087,38 @@ "picomatch": "^2.0.5" } }, + "node-notifier": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", + "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.1.1", + "semver": "^6.3.0", + "shellwords": "^0.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3794,17 +2146,6 @@ "has-flag": "^4.0.0" } }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3822,18 +2163,6 @@ "requires": { "isexe": "^2.0.0" } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } } } }, @@ -4152,44 +2481,266 @@ } }, "@jest/transform": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", - "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.3.0.tgz", + "integrity": "sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/types": "^24.9.0", - "babel-plugin-istanbul": "^5.1.0", - "chalk": "^2.0.1", + "@jest/types": "^25.3.0", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^3.0.0", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.15", - "jest-haste-map": "^24.9.0", - "jest-regex-util": "^24.9.0", - "jest-util": "^24.9.0", - "micromatch": "^3.1.10", + "graceful-fs": "^4.2.3", + "jest-haste-map": "^25.3.0", + "jest-regex-util": "^25.2.6", + "jest-util": "^25.3.0", + "micromatch": "^4.0.2", "pirates": "^4.0.1", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", "source-map": "^0.6.1", - "write-file-atomic": "2.4.1" + "write-file-atomic": "^3.0.0" }, "dependencies": { + "@jest/types": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", + "integrity": "sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.3.0.tgz", + "integrity": "sha512-LjXaRa+F8wwtSxo9G+hHD/Cp63PPQzvaBL9XCVoJD2rrcJO0Zr2+YYzAFWWYJ5GlPUkoaJFJtOuk0sL6MJY80A==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.3", + "jest-serializer": "^25.2.6", + "jest-util": "^25.3.0", + "jest-worker": "^25.2.6", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7", + "which": "^2.0.2" + } + }, + "jest-serializer": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", + "integrity": "sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==", + "dev": true + }, + "jest-util": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.3.0.tgz", + "integrity": "sha512-dc625P/KS/CpWTJJJxKc4bA3A6c+PJGBAqS8JTJqx4HqPoKNqXg/Ec8biL2Z1TabwK7E7Ilf0/ukSEXM1VwzNA==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0", + "chalk": "^3.0.0", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + } + }, + "jest-worker": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.6.tgz", + "integrity": "sha512-FJn9XDUSxcOR4cwDzRfL1z56rUofNTFs539FGASpd50RHdb6EVkhxQqktodW2mI49l+W3H+tFJDotCHUQF6dmA==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "write-file-atomic": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", - "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } } } @@ -4283,20 +2834,20 @@ "integrity": "sha512-qqNWMOsrDjj/daqV21ID2T8mNUjZD4pdx3PuWyE65gzKh2w+oMnzKb+J0NbLyZPn3wwLwU1+Cpf58A0ff5szjQ==" }, "@react-navigation/core": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.5.1.tgz", - "integrity": "sha512-q7NyhWVYOhVIWqL2GZKa6G78YarXaVTTtOlSDkvy4ZIggo40wZzamlnrJRvsaQX46gsgw45FAWb5SriHh8o7eA==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.7.5.tgz", + "integrity": "sha512-B8vKZhHvX+C+lD0UfU5ljCOWIDgDKrlzMuuqIftCB5lnMFvRQZJ5cGpK6u6+BEoX1myfO5j4KUEsRv4H+2f5lg==", "requires": { - "hoist-non-react-statics": "^3.3.0", - "path-to-regexp": "^1.7.0", - "query-string": "^6.4.2", - "react-is": "^16.8.6" + "hoist-non-react-statics": "^3.3.2", + "path-to-regexp": "^1.8.0", + "query-string": "^6.11.1", + "react-is": "^16.13.0" }, "dependencies": { "hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "requires": { "react-is": "^16.7.0" } @@ -4304,19 +2855,18 @@ } }, "@react-navigation/native": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.6.2.tgz", - "integrity": "sha512-Cybeou6N82ZeRmgnGlu+wzlV3z5BZQR2dmYaNFV1TNLUGHqtvv8E7oNw9uYcz9Ox5LFbiX+FdNTn2d6ZPlK0kg==", + "version": "3.7.11", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.7.11.tgz", + "integrity": "sha512-FlnGsLsAVPB9CZ+A0H6Xdv1qTi1Gg39tdXA85Oz0eijb/ilWwvk3x+IKTt/Rex3CU3oEl0ZKwtYC5aSy4CIO7Q==", "requires": { - "hoist-non-react-statics": "^3.0.1", - "react-native-safe-area-view": "^0.14.1", - "react-native-screens": "^1.0.0 || ^1.0.0-alpha" + "hoist-non-react-statics": "^3.3.2", + "react-native-safe-area-view": "^0.14.8" }, "dependencies": { "hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "requires": { "react-is": "^16.7.0" } @@ -4345,21 +2895,6 @@ "node-fetch": "^2.1.2", "progress": "2.0.0", "proxy-from-env": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - } } }, "@sentry/core": { @@ -4509,9 +3044,9 @@ } }, "@types/cheerio": { - "version": "0.22.16", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.16.tgz", - "integrity": "sha512-bSbnU/D4yzFdzLpp3+rcDj0aQQMIRUBNJU7azPxdqMpnexjUSvGJyDuOBQBHeOZh1mMKgsJm6Dy+LLh80Ew4tQ==", + "version": "0.22.17", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.17.tgz", + "integrity": "sha512-izlm+hbqWN9csuB9GSMfCnAyd3/57XZi3rfz1B0C4QBGVMp+9xQ7+9KYnep+ySfUrCWql4lGzkLf0XmprXcz9g==", "dev": true, "requires": { "@types/node": "*" @@ -4558,9 +3093,9 @@ "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==" }, "@types/istanbul-lib-report": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", - "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", "requires": { "@types/istanbul-lib-coverage": "*" } @@ -4585,9 +3120,9 @@ }, "dependencies": { "@jest/types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz", - "integrity": "sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", + "integrity": "sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", @@ -4653,12 +3188,12 @@ "dev": true }, "pretty-format": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", - "integrity": "sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.3.0.tgz", + "integrity": "sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA==", "dev": true, "requires": { - "@jest/types": "^25.1.0", + "@jest/types": "^25.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" @@ -4681,10 +3216,19 @@ "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "dev": true }, + "@types/moment-timezone": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.13.tgz", + "integrity": "sha512-SWk1qM8DRssS5YR9L4eEX7WUhK/wc96aIr4nMa6p0kTk9YhGGOJjECVhIdPEj13fvJw72Xun69gScXSZ/UmcPg==", + "dev": true, + "requires": { + "moment": ">=2.14.0" + } + }, "@types/node": { - "version": "13.1.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", - "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.0.tgz", + "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==", "dev": true }, "@types/prettier": { @@ -4702,6 +3246,7 @@ "version": "16.9.23", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.23.tgz", "integrity": "sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw==", + "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -4725,23 +3270,29 @@ "@types/react": "*" } }, + "@types/shallow-equals": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/shallow-equals/-/shallow-equals-1.0.0.tgz", + "integrity": "sha512-XtGSj7GYPfJwaklDtMEONj+kmpyCP8OLYoPqp/ROM8BL1VaF2IgYbxiEKfLvOyHN7c2d1KAFYzy6EIu8CSFt1A==", + "dev": true + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, "@types/yargs": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz", - "integrity": "sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ==", + "version": "13.0.8", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz", + "integrity": "sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==", "requires": { "@types/yargs-parser": "*" } }, "@types/yargs-parser": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.1.0.tgz", - "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==" + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, "@typescript-eslint/eslint-plugin": { "version": "2.25.0", @@ -4753,67 +3304,18 @@ "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", "tsutils": "^3.17.1" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz", - "integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.25.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz", - "integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^6.3.0", - "tsutils": "^3.17.1" - } - }, - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "regexpp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", - "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } } }, "@typescript-eslint/experimental-utils": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.22.0.tgz", - "integrity": "sha512-sJt1GYBe6yC0dWOQzXlp+tiuGglNhJC9eXZeC8GBVH98Zv9jtatccuhz0OF5kC/DwChqsNfghHx7OlIDQjNYAQ==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz", + "integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.22.0", - "eslint-scope": "^5.0.0" + "@typescript-eslint/typescript-estree": "2.25.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { @@ -4826,56 +3328,12 @@ "@typescript-eslint/experimental-utils": "2.25.0", "@typescript-eslint/typescript-estree": "2.25.0", "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz", - "integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.25.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz", - "integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^6.3.0", - "tsutils": "^3.17.1" - } - }, - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } } }, "@typescript-eslint/typescript-estree": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.22.0.tgz", - "integrity": "sha512-2HFZW2FQc4MhIBB8WhDm9lVFaBDy6h9jGrJ4V2Uzxe/ON29HCHBTj3GkgcsgMWfsl2U5as+pTOr30Nibaw7qRQ==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz", + "integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==", "dev": true, "requires": { "debug": "^4.1.1", @@ -4887,15 +3345,6 @@ "tsutils": "^3.17.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -4963,9 +3412,9 @@ } }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "acorn-walk": { @@ -5008,12 +3457,12 @@ } }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -5160,68 +3609,6 @@ "define-properties": "^1.1.3", "es-abstract": "^1.17.0", "is-string": "^1.0.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "array-map": { @@ -5245,13 +3632,13 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, "array.prototype.find": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz", - "integrity": "sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.1.tgz", + "integrity": "sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.13.0" + "es-abstract": "^1.17.4" } }, "array.prototype.flat": { @@ -5262,62 +3649,6 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "art": { @@ -5392,9 +3723,9 @@ "dev": true }, "aws4": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", - "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", "dev": true }, "babel-eslint": { @@ -5426,36 +3757,6 @@ "slash": "^3.0.0" }, "dependencies": { - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, - "@jest/transform": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.3.0.tgz", - "integrity": "sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.3.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.3.0", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.3.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, "@jest/types": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", @@ -5487,38 +3788,6 @@ "color-convert": "^2.0.1" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -5544,164 +3813,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, - "optional": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "jest-haste-map": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.3.0.tgz", - "integrity": "sha512-LjXaRa+F8wwtSxo9G+hHD/Cp63PPQzvaBL9XCVoJD2rrcJO0Zr2+YYzAFWWYJ5GlPUkoaJFJtOuk0sL6MJY80A==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.3", - "jest-serializer": "^25.2.6", - "jest-util": "^25.3.0", - "jest-worker": "^25.2.6", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - } - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-serializer": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", - "integrity": "sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==", - "dev": true - }, - "jest-util": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.3.0.tgz", - "integrity": "sha512-dc625P/KS/CpWTJJJxKc4bA3A6c+PJGBAqS8JTJqx4HqPoKNqXg/Ec8biL2Z1TabwK7E7Ilf0/ukSEXM1VwzNA==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "chalk": "^3.0.0", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - } - }, - "jest-worker": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.6.tgz", - "integrity": "sha512-FJn9XDUSxcOR4cwDzRfL1z56rUofNTFs539FGASpd50RHdb6EVkhxQqktodW2mI49l+W3H+tFJDotCHUQF6dmA==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -5710,47 +3833,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } } } }, @@ -5763,15 +3845,16 @@ } }, "babel-plugin-istanbul": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", - "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", - "find-up": "^3.0.0", - "istanbul-lib-instrument": "^3.3.0", - "test-exclude": "^5.2.3" + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" } }, "babel-plugin-jest-hoist": { @@ -5794,6 +3877,17 @@ "pkg-up": "^3.1.0", "reselect": "^4.0.0", "resolve": "^1.13.1" + }, + "dependencies": { + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } } }, "babel-plugin-syntax-trailing-function-commas": { @@ -5869,27 +3963,6 @@ "babel-preset-current-node-syntax": "^0.1.2" } }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - } - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -5990,6 +4063,15 @@ "dev": true, "optional": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -6054,9 +4136,9 @@ } }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, "browser-resolve": { @@ -6086,60 +4168,6 @@ "electron-to-chromium": "^1.3.390", "node-releases": "^1.1.53", "pkg-up": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } } }, "bs-logger": { @@ -6163,6 +4191,7 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -6212,9 +4241,9 @@ }, "dependencies": { "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" @@ -6227,9 +4256,9 @@ "dev": true }, "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "requires": { "imurmurhash": "^0.1.4", @@ -6312,6 +4341,46 @@ "htmlparser2": "^3.9.1", "lodash": "^4.15.0", "parse5": "^3.0.1" + }, + "dependencies": { + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + } } }, "chokidar": { @@ -6396,9 +4465,9 @@ "integrity": "sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w==" }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" }, "cliui": { "version": "4.1.0", @@ -6421,9 +4490,9 @@ } }, "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" }, "co": { "version": "4.6.0", @@ -6506,13 +4575,6 @@ "minimist": "~ 1.2.0", "string.prototype.repeat": "^0.2.0", "xregexp": "4.1.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } } }, "commonmark-react-renderer": { @@ -6527,9 +4589,9 @@ } }, "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "compressible": { "version": "2.0.18", @@ -6647,9 +4709,9 @@ } }, "core-js-pure": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz", - "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", + "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==", "dev": true }, "core-util-is": { @@ -6719,15 +4781,14 @@ } }, "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" } }, "css-tree": { @@ -6747,24 +4808,31 @@ } }, "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", + "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==" }, "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, "cssstyle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", - "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz", + "integrity": "sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==", "dev": true, "requires": { - "cssom": "0.3.x" + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } } }, "csstype": { @@ -6790,19 +4858,6 @@ "abab": "^2.0.0", "whatwg-mimetype": "^2.2.0", "whatwg-url": "^7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } } }, "dayjs": { @@ -6810,11 +4865,6 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.24.tgz", "integrity": "sha512-bImQZbBv86zcOWOq6fLg7r4aqMx8fScdmykA7cSh+gH1Yh8AM0Dbw0gHYrsOrza6oBBnkK+/OaR+UAa9UsMrDw==" }, - "debounce": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", - "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -6850,64 +4900,6 @@ "side-channel": "^1.0.1", "which-boxed-primitive": "^1.0.1", "which-collection": "^1.0.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - }, - "dependencies": { - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "requires": { - "has": "^1.0.3" - } - } - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "deep-equal-ident": { @@ -6971,13 +4963,6 @@ "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", "requires": { "clone": "^1.0.2" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - } } }, "define-properties": { @@ -7058,9 +5043,9 @@ "dev": true }, "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, "didyoumean": { @@ -7069,9 +5054,9 @@ "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=" }, "diff-sequences": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz", - "integrity": "sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw==", + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", "dev": true }, "discontinuous-range": { @@ -7090,12 +5075,24 @@ } }, "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" + } } }, "domelementtype": { @@ -7122,10 +5119,9 @@ } }, "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", "requires": { "dom-serializer": "0", "domelementtype": "1" @@ -7147,11 +5143,16 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.412", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.412.tgz", - "integrity": "sha512-4bVdSeJScR8fT7ERveLWbxemY5uXEHVseqMRyORosiKcTUSGtVwBkV8uLjXCqoFLeImA57Z9hbz3TOid01U4Hw==", + "version": "1.3.413", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.413.tgz", + "integrity": "sha512-Jm1Rrd3siqYHO3jftZwDljL2LYQafj3Kki5r+udqE58d0i91SkjItVJ5RwlJn9yko8i7MOcoidVKjQlgSdd1hg==", "dev": true }, + "eme-encryption-scheme-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.1.tgz", + "integrity": "sha512-Wz+Ro1c0/2Wsx2RLFvTOO0m4LvYn+7cSnq3XOvRvLLBq8jbvUACH/zpU9s0/5+mQa5oaelkU69x+q0z/iWYrFA==" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7233,92 +5234,6 @@ "raf": "^3.4.1", "rst-selector-parser": "^2.2.3", "string.prototype.trim": "^1.2.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-boolean-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", - "dev": true - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true - }, - "object.entries": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", - "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "enzyme-adapter-react-16": { @@ -7431,91 +5346,35 @@ } }, "es-abstract": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.2.tgz", - "integrity": "sha512-jYo/J8XU2emLXl3OLwfwtuFfuF2w6DYPs+xy9ZfVyPkDcrauu6LYrw/q2TyCtrbc/KUdCiC5e9UajRhgNkVopA==", - "dev": true, + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", "object-inspect": "^1.7.0", "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" } }, "es-get-iterator": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.0.2.tgz", - "integrity": "sha512-ZHb4fuNK3HKHEOvDGyHPKf5cSWh/OvAMskeM/+21NMnTuvqFvz8uHatolu+7Kf6b6oK9C+3Uo1T37pSGPWv0MA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", "requires": { - "es-abstract": "^1.17.0-next.1", + "es-abstract": "^1.17.4", "has-symbols": "^1.0.1", "is-arguments": "^1.0.4", - "is-map": "^2.0.0", - "is-set": "^2.0.0", - "is-string": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", "isarray": "^2.0.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "requires": { - "has": "^1.0.3" - } - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "es-to-primitive": { @@ -7545,24 +5404,18 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", - "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", + "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", "dev": true, "requires": { - "esprima": "^3.1.3", + "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" }, "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7618,12 +5471,20 @@ }, "dependencies": { "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-regex": { @@ -7632,6 +5493,16 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -7641,42 +5512,63 @@ "restore-cursor": "^3.1.0" } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" } }, "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "requires": { "type-fest": "^0.8.1" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -7688,24 +5580,45 @@ } }, "inquirer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.3.tgz", - "integrity": "sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^3.0.0", "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.15", "mute-stream": "0.0.8", - "run-async": "^2.2.0", + "run-async": "^2.4.0", "rxjs": "^6.5.3", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } } }, "is-fullwidth-code-point": { @@ -7735,6 +5648,12 @@ "mimic-fn": "^2.1.0" } }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7778,6 +5697,21 @@ } } } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -7830,87 +5764,12 @@ "esutils": "^2.0.2" } }, - "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "object.entries": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", - "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, "xregexp": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", @@ -7933,9 +5792,9 @@ } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -7948,13 +5807,13 @@ "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" } }, @@ -7964,12 +5823,20 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } } }, "esrecurse": { @@ -7996,7 +5863,8 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "etag": { "version": "1.8.1", @@ -8213,12 +6081,6 @@ "stack-utils": "^1.0.1" } }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -8367,15 +6229,15 @@ } }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { @@ -8385,11 +6247,11 @@ "dev": true }, "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", "requires": { - "bser": "^2.0.0" + "bser": "2.1.1" } }, "fbjs": { @@ -8436,6 +6298,29 @@ "through2": "^2.0.0" }, "dependencies": { + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", @@ -8486,6 +6371,12 @@ "proper-lockfile": "^1.2.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "filehound": { "version": "1.16.5", "resolved": "https://registry.npmjs.org/filehound/-/filehound-1.16.5.tgz", @@ -8562,7 +6453,7 @@ "dependencies": { "json5": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true } @@ -8644,9 +6535,9 @@ } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "flow-parser": { @@ -8671,9 +6562,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -8775,36 +6666,37 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", + "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" + "node-pre-gyp": "*" }, "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { @@ -8814,13 +6706,13 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "requires": { @@ -8829,39 +6721,39 @@ } }, "chownr": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "optional": true, "requires": { "ms": "^2.1.1" @@ -8869,40 +6761,40 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "^2.6.0" } }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -8917,9 +6809,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": false, - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -8932,13 +6824,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { @@ -8946,9 +6838,9 @@ } }, "ignore-walk": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "optional": true, "requires": { "minimatch": "^3.0.4" @@ -8956,7 +6848,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -8965,20 +6857,20 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "optional": true, "requires": { @@ -8987,13 +6879,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "optional": true, "requires": { @@ -9001,15 +6893,15 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "optional": true }, "minipass": { - "version": "2.3.5", - "resolved": false, - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "optional": true, "requires": { "safe-buffer": "^5.1.2", @@ -9017,44 +6909,44 @@ } }, "minizlib": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "^2.9.0" } }, "mkdirp": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", "optional": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "ms": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, "needle": { - "version": "2.3.0", - "resolved": false, - "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.3.tgz", + "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==", "optional": true, "requires": { - "debug": "^4.1.0", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.12.0", - "resolved": false, - "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", + "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", "optional": true, "requires": { "detect-libc": "^1.0.2", @@ -9066,13 +6958,13 @@ "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^4" + "tar": "^4.4.2" } }, "nopt": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "optional": true, "requires": { "abbrev": "1", @@ -9080,24 +6972,34 @@ } }, "npm-bundled": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "optional": true }, "npm-packlist": { - "version": "1.4.1", - "resolved": false, - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "optional": true, "requires": { "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" } }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -9109,19 +7011,19 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "optional": true, "requires": { @@ -9130,19 +7032,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -9152,19 +7054,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { @@ -9172,20 +7074,12 @@ "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "optional": true - } } }, "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -9198,9 +7092,9 @@ } }, "rimraf": { - "version": "2.6.3", - "resolved": false, - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "optional": true, "requires": { "glob": "^7.1.3" @@ -9208,43 +7102,43 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { - "version": "5.7.0", - "resolved": false, - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "optional": true, "requires": { @@ -9255,7 +7149,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -9264,7 +7158,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "optional": true, "requires": { @@ -9273,34 +7167,34 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { - "version": "4.4.8", - "resolved": false, - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" } }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { @@ -9309,14 +7203,14 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "optional": true }, "yallist": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "optional": true } } @@ -9335,62 +7229,6 @@ "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1", "functions-have-names": "^1.2.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "functional-red-black-tree": { @@ -9400,9 +7238,9 @@ "dev": true }, "functions-have-names": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.0.tgz", - "integrity": "sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", + "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", "dev": true }, "fuse.js": { @@ -9423,7 +7261,8 @@ "get-params": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", - "integrity": "sha1-uuDfq6WIoMYNeDTA2Nwv9g7u8v4=" + "integrity": "sha1-uuDfq6WIoMYNeDTA2Nwv9g7u8v4=", + "dev": true }, "get-stream": { "version": "4.1.0", @@ -9447,14 +7286,6 @@ "assert-plus": "^1.0.0" } }, - "gfycat-sdk": { - "version": "1.4.18", - "resolved": "https://registry.npmjs.org/gfycat-sdk/-/gfycat-sdk-1.4.18.tgz", - "integrity": "sha512-BrINtO6rj8Nr0pm38Qr3epayOuvlKcEFcDCw6yL9T4SrsWTECct6FS/isli766kdij2GGG6bWU6bRp+fDS2Cbg==", - "requires": { - "babel-runtime": "^6.23.0" - } - }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -9611,9 +7442,9 @@ } }, "hasha": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", - "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", "dev": true, "requires": { "is-stream": "^2.0.0", @@ -9625,6 +7456,12 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -9648,9 +7485,9 @@ } }, "hosted-git-info": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==" + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, "html-element-map": { "version": "1.2.0", @@ -9679,9 +7516,9 @@ } }, "html-escaper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", - "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, "htmlparser2": { @@ -9699,9 +7536,9 @@ }, "dependencies": { "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -9760,7 +7597,8 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true }, "ignore": { "version": "4.0.6", @@ -9783,13 +7621,58 @@ } }, "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", "dev": true, "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } } }, "imurmurhash": { @@ -9857,62 +7740,6 @@ "es-abstract": "^1.17.0-next.1", "has": "^1.0.3", "side-channel": "^1.0.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "interpret": { @@ -10015,9 +7842,9 @@ } }, "is-boolean-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", - "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==" }, "is-buffer": { "version": "1.1.6", @@ -10025,9 +7852,9 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-ci": { "version": "2.0.0", @@ -10056,9 +7883,9 @@ } }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, "is-descriptor": { "version": "0.1.6", @@ -10143,9 +7970,9 @@ } }, "is-number-object": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", - "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==" }, "is-plain-object": { "version": "2.0.4", @@ -10161,11 +7988,11 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "requires": { - "has": "^1.0.1" + "has": "^1.0.3" } }, "is-set": { @@ -10179,9 +8006,9 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", - "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" }, "is-subset": { "version": "0.1.1", @@ -10223,9 +8050,9 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" }, "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "isexe": { "version": "2.0.0", @@ -10264,9 +8091,9 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", "dev": true }, "istanbul-lib-hook": { @@ -10279,18 +8106,18 @@ } }, "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", + "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", "dev": true, "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" }, "dependencies": { "semver": { @@ -10317,9 +8144,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -10327,16 +8154,10 @@ "which": "^2.0.1" } }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" @@ -10349,9 +8170,9 @@ "dev": true }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -10406,12 +8227,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, "make-dir": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", @@ -10449,12 +8264,6 @@ "source-map": "^0.6.1" }, "dependencies": { - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10487,76 +8296,6 @@ "@jest/core": "^25.1.0", "import-local": "^3.0.2", "jest-cli": "^25.1.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } } }, "jest-changed-files": { @@ -10922,16 +8661,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -11003,15 +8732,6 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, "pretty-format": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.3.0.tgz", @@ -11024,27 +8744,21 @@ "react-is": "^16.12.0" } }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -11160,60 +8874,6 @@ "realpath-native": "^2.0.0" }, "dependencies": { - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, - "@jest/environment": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.3.0.tgz", - "integrity": "sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.3.0", - "@jest/types": "^25.3.0", - "jest-mock": "^25.3.0" - } - }, - "@jest/fake-timers": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", - "integrity": "sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "jest-message-util": "^25.3.0", - "jest-mock": "^25.3.0", - "jest-util": "^25.3.0", - "lolex": "^5.0.0" - } - }, - "@jest/transform": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.3.0.tgz", - "integrity": "sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.3.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.3.0", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.3.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, "@jest/types": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", @@ -11251,16 +8911,6 @@ "color-convert": "^2.0.1" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, "babel-jest": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.3.0.tgz", @@ -11276,19 +8926,6 @@ "slash": "^3.0.0" } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -11329,29 +8966,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz", - "integrity": "sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -11361,13 +8975,6 @@ "to-regex-range": "^5.0.1" } }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, - "optional": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -11380,103 +8987,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "jest-environment-jsdom": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.3.0.tgz", - "integrity": "sha512-jdE4bQN+k2QEZ9sWOxsqDJvMzbdFSCN/4tw8X0TQaCqyzKz58PyEf41oIr4WO7ERdp7WaJGBSUKF7imR3UW1lg==", - "dev": true, - "requires": { - "@jest/environment": "^25.3.0", - "@jest/fake-timers": "^25.3.0", - "@jest/types": "^25.3.0", - "jest-mock": "^25.3.0", - "jest-util": "^25.3.0", - "jsdom": "^15.2.1" - } - }, "jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", "dev": true }, - "jest-haste-map": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.3.0.tgz", - "integrity": "sha512-LjXaRa+F8wwtSxo9G+hHD/Cp63PPQzvaBL9XCVoJD2rrcJO0Zr2+YYzAFWWYJ5GlPUkoaJFJtOuk0sL6MJY80A==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.3", - "jest-serializer": "^25.2.6", - "jest-util": "^25.3.0", - "jest-worker": "^25.2.6", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - } - }, - "jest-message-util": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.3.0.tgz", - "integrity": "sha512-5QNy9Id4WxJbRITEbA1T1kem9bk7y2fD0updZMSTNHtbEDnYOGLDPAuFBhFgVmOZpv0n6OMdVkK+WhyXEPCcOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.3.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.3.0.tgz", - "integrity": "sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0" - } - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-serializer": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", - "integrity": "sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==", - "dev": true - }, "jest-util": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.3.0.tgz", @@ -11503,50 +9019,6 @@ "pretty-format": "^25.3.0" } }, - "jest-worker": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.6.tgz", - "integrity": "sha512-FJn9XDUSxcOR4cwDzRfL1z56rUofNTFs539FGASpd50RHdb6EVkhxQqktodW2mI49l+W3H+tFJDotCHUQF6dmA==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, "make-dir": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", @@ -11556,12 +9028,6 @@ "semver": "^6.0.0" } }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -11572,18 +9038,6 @@ "picomatch": "^2.0.5" } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, "pretty-format": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.3.0.tgz", @@ -11596,12 +9050,6 @@ "react-is": "^16.12.0" } }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -11614,12 +9062,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -11629,17 +9071,6 @@ "has-flag": "^4.0.0" } }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11648,68 +9079,25 @@ "requires": { "is-number": "^7.0.0" } - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } } } }, "jest-diff": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.1.0.tgz", - "integrity": "sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.3.0.tgz", + "integrity": "sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w==", "dev": true, "requires": { "chalk": "^3.0.0", - "diff-sequences": "^25.1.0", - "jest-get-type": "^25.1.0", - "pretty-format": "^25.1.0" + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.3.0" }, "dependencies": { "@jest/types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz", - "integrity": "sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", + "integrity": "sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", @@ -11775,18 +9163,18 @@ "dev": true }, "jest-get-type": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz", - "integrity": "sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw==", + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", "dev": true }, "pretty-format": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", - "integrity": "sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.3.0.tgz", + "integrity": "sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA==", "dev": true, "requires": { - "@jest/types": "^25.1.0", + "@jest/types": "^25.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" @@ -11810,14 +9198,6 @@ "dev": true, "requires": { "detect-newline": "^3.0.0" - }, - "dependencies": { - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - } } }, "jest-each": { @@ -11964,20 +9344,486 @@ "dev": true, "requires": { "jest-environment-jsdom": "^24.0.0" + }, + "dependencies": { + "@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + } + }, + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + } + }, + "jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } } }, "jest-environment-jsdom": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", - "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.3.0.tgz", + "integrity": "sha512-jdE4bQN+k2QEZ9sWOxsqDJvMzbdFSCN/4tw8X0TQaCqyzKz58PyEf41oIr4WO7ERdp7WaJGBSUKF7imR3UW1lg==", "dev": true, "requires": { - "@jest/environment": "^24.9.0", - "@jest/fake-timers": "^24.9.0", - "@jest/types": "^24.9.0", - "jest-mock": "^24.9.0", - "jest-util": "^24.9.0", - "jsdom": "^11.5.1" + "@jest/environment": "^25.3.0", + "@jest/fake-timers": "^25.3.0", + "@jest/types": "^25.3.0", + "jest-mock": "^25.3.0", + "jest-util": "^25.3.0", + "jsdom": "^15.2.1" + }, + "dependencies": { + "@jest/fake-timers": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", + "integrity": "sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0", + "jest-message-util": "^25.3.0", + "jest-mock": "^25.3.0", + "jest-util": "^25.3.0", + "lolex": "^5.0.0" + } + }, + "@jest/types": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", + "integrity": "sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-message-util": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.3.0.tgz", + "integrity": "sha512-5QNy9Id4WxJbRITEbA1T1kem9bk7y2fD0updZMSTNHtbEDnYOGLDPAuFBhFgVmOZpv0n6OMdVkK+WhyXEPCcOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.3.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.3.0.tgz", + "integrity": "sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0" + } + }, + "jest-util": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.3.0.tgz", + "integrity": "sha512-dc625P/KS/CpWTJJJxKc4bA3A6c+PJGBAqS8JTJqx4HqPoKNqXg/Ec8biL2Z1TabwK7E7Ilf0/ukSEXM1VwzNA==", + "dev": true, + "requires": { + "@jest/types": "^25.3.0", + "chalk": "^3.0.0", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + } + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } } }, "jest-environment-node": { @@ -11994,17 +9840,6 @@ "semver": "^6.3.0" }, "dependencies": { - "@jest/environment": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.3.0.tgz", - "integrity": "sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.3.0", - "@jest/types": "^25.3.0", - "jest-mock": "^25.3.0" - } - }, "@jest/fake-timers": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", @@ -12263,30 +10098,6 @@ "slash": "^3.0.0" } }, - "@jest/environment": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.3.0.tgz", - "integrity": "sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.3.0", - "@jest/types": "^25.3.0", - "jest-mock": "^25.3.0" - } - }, - "@jest/fake-timers": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", - "integrity": "sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "jest-message-util": "^25.3.0", - "jest-mock": "^25.3.0", - "jest-util": "^25.3.0", - "lolex": "^5.0.0" - } - }, "@jest/source-map": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.6.tgz", @@ -12423,15 +10234,6 @@ "stack-utils": "^1.0.1" } }, - "jest-mock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.3.0.tgz", - "integrity": "sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0" - } - }, "jest-util": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.3.0.tgz", @@ -12700,30 +10502,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-diff": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.3.0.tgz", - "integrity": "sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.3.0" - } - }, "jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", @@ -12783,9 +10567,9 @@ "dev": true }, "jest-regex-util": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", - "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", "dev": true }, "jest-resolve": { @@ -12864,21 +10648,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "resolve": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.1.tgz", - "integrity": "sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -12963,12 +10732,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -13019,30 +10782,6 @@ "slash": "^3.0.0" } }, - "@jest/environment": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.3.0.tgz", - "integrity": "sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.3.0", - "@jest/types": "^25.3.0", - "jest-mock": "^25.3.0" - } - }, - "@jest/fake-timers": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", - "integrity": "sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "jest-message-util": "^25.3.0", - "jest-mock": "^25.3.0", - "jest-util": "^25.3.0", - "lolex": "^5.0.0" - } - }, "@jest/source-map": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.6.tgz", @@ -13210,15 +10949,6 @@ "stack-utils": "^1.0.1" } }, - "jest-mock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.3.0.tgz", - "integrity": "sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0" - } - }, "jest-serializer": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", @@ -13364,12 +11094,6 @@ "yargs": "^15.3.1" }, "dependencies": { - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, "@jest/console": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.3.0.tgz", @@ -13382,30 +11106,6 @@ "slash": "^3.0.0" } }, - "@jest/environment": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.3.0.tgz", - "integrity": "sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.3.0", - "@jest/types": "^25.3.0", - "jest-mock": "^25.3.0" - } - }, - "@jest/fake-timers": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.3.0.tgz", - "integrity": "sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==", - "dev": true, - "requires": { - "@jest/types": "^25.3.0", - "jest-message-util": "^25.3.0", - "jest-mock": "^25.3.0", - "jest-util": "^25.3.0", - "lolex": "^5.0.0" - } - }, "@jest/source-map": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.6.tgz", @@ -13429,30 +11129,6 @@ "collect-v8-coverage": "^1.0.0" } }, - "@jest/transform": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.3.0.tgz", - "integrity": "sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.3.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.3.0", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.3.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, "@jest/types": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.3.0.tgz", @@ -13500,19 +11176,6 @@ "picomatch": "^2.0.4" } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -13620,27 +11283,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, "jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", @@ -13691,12 +11333,6 @@ "@jest/types": "^25.3.0" } }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, "jest-serializer": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", @@ -13806,12 +11442,6 @@ "react-is": "^16.12.0" } }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -13871,17 +11501,6 @@ "has-flag": "^4.0.0" } }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13911,18 +11530,6 @@ "strip-ansi": "^6.0.0" } }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, "yargs": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", @@ -14052,12 +11659,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -14079,18 +11680,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "jest-diff": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.3.0.tgz", - "integrity": "sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.3.0" - } - }, "jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", @@ -14454,7 +12043,8 @@ "jsan": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.13.tgz", - "integrity": "sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==" + "integrity": "sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==", + "dev": true }, "jsbn": { "version": "0.1.1", @@ -14468,59 +12058,50 @@ "integrity": "sha512-wkjURqwaB1daNkDi2OYYbsLnIdC/lUM2nPXQKRs5pqEU9chDg435bjvo+LSaHotDENygHQDHe+ntUkkw2gwMtg==" }, "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", "dev": true, "requires": { "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", "domexception": "^1.0.1", - "escodegen": "^1.9.1", + "escodegen": "^1.11.1", "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", + "tough-cookie": "^3.0.1", "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", "xml-name-validator": "^3.0.0" }, "dependencies": { - "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", - "dev": true - }, "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", + "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==", + "dev": true } } }, @@ -14572,18 +12153,11 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "requires": { - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } + "minimist": "^1.2.5" } }, "jsonfile": { @@ -14695,7 +12269,8 @@ "linked-list": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", - "integrity": "sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78=" + "integrity": "sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78=", + "dev": true }, "load-json-file": { "version": "2.0.0", @@ -14742,12 +12317,6 @@ "requires": { "minimist": "^1.2.0" } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true } } }, @@ -14997,79 +12566,6 @@ "object-visit": "^1.0.0" } }, - "mattermost-redux": { - "version": "github:mattermost/mattermost-redux#0f77cd9b13d4ce5f08d425cc1b7a0d65f0338a51", - "from": "github:mattermost/mattermost-redux#0f77cd9b13d4ce5f08d425cc1b7a0d65f0338a51", - "requires": { - "core-js": "3.1.4", - "form-data": "2.5.1", - "gfycat-sdk": "1.4.18", - "isomorphic-fetch": "2.2.1", - "moment-timezone": "0.5.26", - "redux": "4.0.4", - "redux-action-buffer": "1.2.0", - "redux-offline": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "redux-persist": "4.9.1", - "redux-persist-node-storage": "2.0.0", - "redux-thunk": "2.3.0", - "remote-redux-devtools": "0.5.16", - "reselect": "4.0.0", - "serialize-error": "5.0.0", - "shallow-equals": "1.0.0" - }, - "dependencies": { - "core-js": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", - "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==" - }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "moment-timezone": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz", - "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==", - "requires": { - "moment": ">= 2.9.0" - } - }, - "redux": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", - "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, - "redux-offline": { - "version": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "from": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "requires": { - "@react-native-community/netinfo": "^4.1.3", - "redux-persist": "^4.5.0" - } - }, - "redux-persist": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.9.1.tgz", - "integrity": "sha512-XoOmfPyo9GEU/WLH9FgB47dNIN9l5ArjHes4o7vUWx9nxZoPxnVodhuHdyc4Ot+fMkdj3L2LTqSHhwrkr0QFUg==", - "requires": { - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.4", - "lodash-es": "^4.17.4" - } - } - } - }, "mdn-data": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", @@ -15175,10 +12671,38 @@ "yargs": "^9.0.0" }, "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } }, "@babel/plugin-transform-runtime": { "version": "7.9.0", @@ -15239,6 +12763,13 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, "execa": { @@ -15362,11 +12893,6 @@ "mime-db": "~1.23.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", @@ -15395,15 +12921,6 @@ "ansi-regex": "^2.0.0" } }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" - } - }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", @@ -15458,6 +12975,29 @@ "escape-string-regexp": "^1.0.5" }, "dependencies": { + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, "@babel/register": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.9.0.tgz", @@ -15474,6 +13014,11 @@ "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -15484,6 +13029,36 @@ "requires": { "@babel/core": "^7.0.0", "metro-source-map": "^0.56.4" + }, + "dependencies": { + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "metro-cache": { @@ -15662,20 +13237,6 @@ "ansi-regex": "^2.0.0" } }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" - }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" - } - }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", @@ -15774,10 +13335,28 @@ "metro-source-map": "^0.56.4" }, "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } }, "@babel/plugin-transform-runtime": { "version": "7.9.0", @@ -15904,18 +13483,11 @@ "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" }, "mime-types": { - "version": "2.1.25", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", - "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", "requires": { - "mime-db": "1.42.0" - }, - "dependencies": { - "mime-db": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" - } + "mime-db": "1.43.0" } }, "mimic-fn": { @@ -15932,9 +13504,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mixin-deep": { "version": "1.3.2", @@ -15956,11 +13528,11 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "mmjstool": { @@ -16008,17 +13580,17 @@ "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "moment-timezone": { - "version": "0.5.28", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.28.tgz", - "integrity": "sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==", + "version": "0.5.26", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz", + "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==", "requires": { "moment": ">= 2.9.0" } }, "moo": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", - "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", + "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", "dev": true }, "morgan": { @@ -16072,7 +13644,8 @@ "nanoid": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", + "dev": true }, "nanomatch": { "version": "1.2.13", @@ -16099,13 +13672,13 @@ "dev": true }, "nearley": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.0.tgz", - "integrity": "sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA==", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.2.tgz", + "integrity": "sha512-h6lygT0BWAGErDvoE2LfI+tDeY2+UUrqG5dcBPdCmjnjud9z1wE0P7ljb85iNbE93YA+xJLpoSYGMuUqhnSSSA==", "dev": true, "requires": { "commander": "^2.19.0", - "moo": "^0.4.3", + "moo": "^0.5.0", "railroad-diagrams": "^1.0.0", "randexp": "0.4.6", "semver": "^5.4.1" @@ -16155,6 +13728,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-1.3.1.tgz", "integrity": "sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==", + "dev": true, "requires": { "write-file-atomic": "^1.1.4" } @@ -16165,32 +13739,21 @@ "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" }, "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", - "dev": true, - "optional": true, + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", "requires": { "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", "shellwords": "^0.1.1", - "which": "^1.3.1" + "which": "^1.3.0" }, "dependencies": { - "is-wsl": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", - "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", - "dev": true, - "optional": true - }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "optional": true + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -16303,12 +13866,6 @@ "yargs": "^15.0.2" }, "dependencies": { - "@babel/parser": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.0.tgz", - "integrity": "sha512-VVtsnUYbd1+2A2vOVhm4P2qNXQE8L/W859GpUHfUcdhX8d3pEKThZuIr6fztocWx9HbK+00/CR0tXnhAggJ4CA==", - "dev": true - }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -16351,23 +13908,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^3.0.0", + "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, @@ -16387,71 +13935,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -16462,9 +13951,9 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" @@ -16507,9 +13996,9 @@ "dev": true }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -16521,12 +14010,6 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -16547,26 +14030,6 @@ "ansi-regex": "^5.0.0" } }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -16579,9 +14042,9 @@ } }, "yargs": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", - "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -16594,13 +14057,13 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^16.1.0" + "yargs-parser": "^18.1.1" } }, "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -16659,9 +14122,13 @@ "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" }, "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } }, "object-keys": { "version": "1.1.1", @@ -16688,13 +14155,13 @@ } }, "object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", + "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", + "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" } @@ -16709,72 +14176,16 @@ "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, "object.pick": { @@ -16795,62 +14206,6 @@ "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "on-finished": { @@ -16967,9 +14322,9 @@ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" }, "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "requires": { "p-try": "^2.0.0" } @@ -17084,12 +14439,6 @@ "tmp": "^0.0.33" }, "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -17189,12 +14538,57 @@ } }, "pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", "dev": true, "requires": { - "find-up": "^3.0.0" + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } } }, "plist": { @@ -17381,9 +14775,9 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.5.0.tgz", - "integrity": "sha512-4vqUjKi2huMu1OJiLhi3jN6jeeKvMZdI1tYgi/njW5zV52jNLgSAZSdN16m9bJFe61/cT8ulmw4qFitV9QRsEA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, "pump": { @@ -17408,9 +14802,9 @@ "dev": true }, "query-string": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.9.0.tgz", - "integrity": "sha512-KG4bhCFYapExLsUHrFt+kQVEegF2agm4cpF/VNc6pZVthIfCc/GK8t8VyNIE3nyXG9DK3Tf2EGkxjR6/uRdYsA==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.12.1.tgz", + "integrity": "sha512-OHj+zzfRMyj3rmo/6G8a5Ifvw3AleL/EbcHMD27YA31Q+cO5lfmQxECkImuNVjcskLcvBRVHNAB3w6udMs1eAA==", "requires": { "decode-uri-component": "^0.2.0", "split-on-first": "^1.0.0", @@ -17420,7 +14814,8 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true }, "querystringify": { "version": "2.1.1", @@ -17540,9 +14935,9 @@ } }, "react-is": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", - "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-lifecycles-compat": { "version": "2.0.0", @@ -17784,30 +15179,6 @@ "p-locate": "^4.1.0" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node-notifier": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", - "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -17833,15 +15204,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" - } } } }, @@ -18058,24 +15420,27 @@ "integrity": "sha512-gBLv93P90sM6hk5HzUwTXzFuSDazTpg2ONi5iL9pnUsUfwdw2L9SKgjgVroxX10leGB9+0zz6/ycV+mItqr8OQ==", "requires": { "@types/react": "^16.8.8" + }, + "dependencies": { + "@types/react": { + "version": "16.9.34", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.34.tgz", + "integrity": "sha512-8AJlYMOfPe1KGLKyHpflCg5z46n0b5DbRfqDksxBLBTUpB75ypDBAO9eCUcjNwE6LCUslwTz00yyG/X9gaVtow==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + } } }, "react-native-safe-area-view": { - "version": "0.14.8", - "resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.14.8.tgz", - "integrity": "sha512-MtRSIcZNstxv87Jet+UsPhEd1tpGe8cVskDXlP657x6rHpSrbrc+y13ZNXrwAgGNNhqQNX7UJT68ZIq//ZRmvw==", + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.14.9.tgz", + "integrity": "sha512-WII/ulhpVyL/qbYb7vydq7dJAfZRBcEhg4/UWt6F6nAKpLa3gAceMOxBxI914ppwSP/TdUsandFy6lkJQE0z4A==", "requires": { "hoist-non-react-statics": "^2.3.1" } }, - "react-native-screens": { - "version": "1.0.0-alpha.23", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-1.0.0-alpha.23.tgz", - "integrity": "sha512-tOxHGQUN83MTmQB4ghoQkibqOdGiX4JQEmeyEv96MKWO/x8T2PJv84ECUos9hD3blPRQwVwSpAid1PPPhrVEaw==", - "requires": { - "debounce": "^1.2.0" - } - }, "react-native-section-list-get-item-layout": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-native-section-list-get-item-layout/-/react-native-section-list-get-item-layout-2.2.3.tgz", @@ -18101,33 +15466,6 @@ "requires": { "css-select": "^2.1.0", "css-tree": "^1.0.0-alpha.39" - }, - "dependencies": { - "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-what": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", - "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==" - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - } } }, "react-native-v8": { @@ -18194,9 +15532,9 @@ } }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -18207,13 +15545,13 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -18388,9 +15726,9 @@ } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -18399,6 +15737,13 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } } }, "readdirp": { @@ -18414,13 +15759,10 @@ } }, "realpath-native": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", - "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", - "dev": true, - "requires": { - "util.promisify": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", + "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", + "dev": true }, "redux": { "version": "4.0.5", @@ -18429,6 +15771,13 @@ "requires": { "loose-envify": "^1.4.0", "symbol-observable": "^1.2.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + } } }, "redux-action-buffer": { @@ -18445,6 +15794,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/redux-devtools-core/-/redux-devtools-core-0.2.1.tgz", "integrity": "sha512-RAGOxtUFdr/1USAvxrWd+Gq/Euzgw7quCZlO5TgFpDfG7rB5tMhZUrNyBjpzgzL2yMk0eHnPYIGm7NkIfRzHxQ==", + "dev": true, "requires": { "get-params": "^0.1.2", "jsan": "^3.1.13", @@ -18457,9 +15807,18 @@ "version": "1.9.6", "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz", "integrity": "sha512-MwvY4cLEB2tIfWWBzrUR02UM9qRG2i7daNzywRvabOSVdvAY7s9BxSwMmVRH1Y/7QWjplNtOwgT0apKhHg2Qew==", + "dev": true, "requires": { "lodash": "^4.2.0", "symbol-observable": "^1.0.2" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + } } }, "redux-mock-store": { @@ -18493,6 +15852,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/redux-persist-node-storage/-/redux-persist-node-storage-2.0.0.tgz", "integrity": "sha1-QAHjK4tDxzgH7y2pujAfSxxmr3k=", + "dev": true, "requires": { "node-localstorage": "^1.3.0" } @@ -18565,63 +15925,12 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "regexpu-core": { @@ -18670,6 +15979,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz", "integrity": "sha512-xZ2D1VRIWzat5nsvcraT6fKEX9Cfi+HbQBCwzNnUAM8Uicm/anOc60XGalcaDPrVmLug7nhDl2nimEa3bL3K9w==", + "dev": true, "requires": { "jsan": "^3.1.13", "querystring": "^0.2.0", @@ -18683,6 +15993,7 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/remotedev-serialize/-/remotedev-serialize-0.1.8.tgz", "integrity": "sha512-3YG/FDcOmiK22bl5oMRM8RRnbGrFEuPGjbcDG+z2xi5aQaNQNZ8lqoRnZTwXVfaZtutXuiAQOgPRrogzQk8edg==", + "dev": true, "requires": { "jsan": "^3.1.13" } @@ -18703,9 +16014,9 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -18715,7 +16026,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -18725,7 +16036,7 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, @@ -18741,20 +16052,14 @@ "mime-types": "^2.1.12" } }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" } } } @@ -18777,6 +16082,18 @@ "request-promise-core": "1.1.3", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } } }, "require-directory": { @@ -18800,20 +16117,28 @@ "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" }, "resolve": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", - "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.1.tgz", + "integrity": "sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==", "requires": { "path-parse": "^1.0.6" } }, "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "requires": { - "resolve-from": "^3.0.0" + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } } }, "resolve-dir": { @@ -18904,7 +16229,8 @@ "rn-host-detect": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rn-host-detect/-/rn-host-detect-1.2.0.tgz", - "integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==" + "integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==", + "dev": true }, "rn-placeholder": { "version": "github:mattermost/rn-placeholder#02c629c65d0123a2eee623ada0fd17186415d3c3", @@ -18940,9 +16266,9 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", "requires": { "is-promise": "^2.1.0" } @@ -18961,9 +16287,9 @@ } }, "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", "requires": { "tslib": "^1.9.0" } @@ -19000,13 +16326,6 @@ "micromatch": "^3.1.4", "minimist": "^1.1.1", "walker": "~1.0.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } } }, "sax": { @@ -19027,19 +16346,30 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", + "dev": true, "requires": { "component-emitter": "1.2.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + } } }, "sc-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-2.0.1.tgz", - "integrity": "sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ==" + "integrity": "sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ==", + "dev": true }, "sc-formatter": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.2.tgz", - "integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==" + "integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==", + "dev": true }, "scheduler": { "version": "0.15.0", @@ -19103,6 +16433,13 @@ "integrity": "sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA==", "requires": { "type-fest": "^0.8.0" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + } } }, "serve-static": { @@ -19153,9 +16490,12 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "shaka-player": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.6.tgz", - "integrity": "sha512-z1BM/kpg5OVk57w5gXYBGk78Le04gfCHBOpT106Nz7h4VU7im7o8ZldUKxC9+FlSL4sPvdU8lY6OxL7L3hw5Fw==" + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.10.tgz", + "integrity": "sha512-kS9TQL6bWODo4XNnozERZWsEiWlLZ6huspPx4ZjmMjeOBL9gwqlULLfLyO+5gA3CYV/dk9LaAi1WAEaLWckGpA==", + "requires": { + "eme-encryption-scheme-polyfill": "^2.0.1" + } }, "shallow-equals": { "version": "1.0.0", @@ -19198,63 +16538,12 @@ "requires": { "es-abstract": "^1.17.0-next.1", "object-inspect": "^1.7.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "simple-plist": { "version": "1.1.0", @@ -19417,20 +16706,13 @@ "inquirer": "^6.2.1", "minimist": "^1.2.0", "yamljs": "^0.3.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } } }, "socketcluster-client": { "version": "14.3.1", "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-14.3.1.tgz", "integrity": "sha512-Sd/T0K/9UlqTfz+HUuFq90dshA5OBJPQbdkRzGtcKIOm52fkdsBTt0FYpiuzzxv5VrU7PWpRm6KIfNXyPwlLpw==", + "dev": true, "requires": { "buffer": "^5.2.1", "clone": "2.1.1", @@ -19444,10 +16726,32 @@ "ws": "7.1.0" }, "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, "uuid": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "ws": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", + "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", + "dev": true, + "requires": { + "async-limiter": "^1.0.0" + } } } }, @@ -19462,10 +16766,10 @@ "minimist": "^1.2.0" }, "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", "dev": true } } @@ -19476,11 +16780,11 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "requires": { - "atob": "^2.1.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -19523,18 +16827,18 @@ }, "dependencies": { "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" } }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -19631,13 +16935,6 @@ "integrity": "sha512-DRy03ljj0367Ud3OAJHD6eVS/+CvMK2u/djVYuU37fHYcYHoZ8tkFyhbRf7PNG1h3bWLsw+SNTSXrPFe07A7aQ==", "requires": { "type-fest": "^0.7.1" - }, - "dependencies": { - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" - } } }, "static-extend": { @@ -19721,62 +17018,6 @@ "internal-slot": "^1.0.2", "regexp.prototype.flags": "^1.3.0", "side-channel": "^1.0.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "string.prototype.repeat": { @@ -19793,82 +17034,44 @@ "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" } }, "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string_decoder": { @@ -19911,9 +17114,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", "dev": true }, "sudo-prompt": { @@ -19957,9 +17160,9 @@ } }, "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" }, "symbol-tree": { "version": "3.2.4", @@ -20048,71 +17251,14 @@ } }, "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - }, - "dependencies": { - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - } + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" } }, "text-table": { @@ -20212,11 +17358,12 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", "dev": true, "requires": { + "ip-regex": "^2.1.0", "psl": "^1.1.28", "punycode": "^2.1.1" } @@ -20267,9 +17414,9 @@ } }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, "tsutils": { "version": "3.17.1", @@ -20311,9 +17458,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" }, "type-of": { "version": "2.0.1", @@ -20463,6 +17610,11 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" } } }, @@ -20519,13 +17671,15 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" } }, "utils-merge": { @@ -20534,9 +17688,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-android": { "version": "8.80.1", @@ -20599,12 +17753,12 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==" }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "dev": true, "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "w3c-xmlserializer": { @@ -20619,9 +17773,9 @@ } }, "wait-for-expect": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.1.tgz", - "integrity": "sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", + "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==", "dev": true }, "walker": { @@ -20688,12 +17842,31 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -20783,9 +17956,9 @@ "dev": true }, "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, "requires": { "lodash.sortby": "^4.7.0", @@ -20814,14 +17987,14 @@ } }, "which-collection": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.0.tgz", - "integrity": "sha512-mG4RtFHE+17N2AxRNvBQ488oBjrhaOaI/G+soUaRJwdyDbu5zmqoAKPYBlY7Zd+QTwpfvInRLKo40feo2si1yA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", "requires": { - "is-map": "^2.0.0", - "is-set": "^2.0.0", - "is-weakmap": "^2.0.0", - "is-weakset": "^2.0.0" + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" } }, "which-module": { @@ -20907,11 +18080,12 @@ } }, "ws": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", - "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", "requires": { - "async-limiter": "^1.0.0" + "options": ">=0.0.5", + "ultron": "1.0.x" } }, "xcode": { diff --git a/package.json b/package.json index 39485202d..93ecd38d3 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,8 @@ "fuse.js": "3.6.1", "intl": "1.2.5", "jail-monkey": "2.3.2", - "mattermost-redux": "github:mattermost/mattermost-redux#0f77cd9b13d4ce5f08d425cc1b7a0d65f0338a51", "mime-db": "1.43.0", - "moment-timezone": "0.5.28", + "moment-timezone": "0.5.26", "prop-types": "15.7.2", "react": "16.13.0", "react-intl": "2.8.0", @@ -69,6 +68,7 @@ "react-navigation-stack": "1.9.4", "react-redux": "7.2.0", "redux": "4.0.5", + "redux-action-buffer": "1.2.0", "redux-batched-actions": "0.4.1", "redux-offline": "github:mattermost/redux-offline#885024de96b6ec73650c340c8928066585c413df", "redux-persist": "4.10.2", @@ -78,6 +78,7 @@ "rn-fetch-blob": "0.12.0", "rn-placeholder": "github:mattermost/rn-placeholder#02c629c65d0123a2eee623ada0fd17186415d3c3", "semver": "7.1.3", + "serialize-error": "5.0.0", "shallow-equals": "1.0.0", "tinycolor2": "1.4.1", "url-parse": "1.4.7" @@ -92,9 +93,11 @@ "@types/enzyme": "3.10.5", "@types/enzyme-adapter-react-16": "1.0.6", "@types/jest": "25.1.4", + "@types/moment-timezone": "0.5.13", "@types/react": "16.9.23", "@types/react-native": "0.61.23", "@types/react-test-renderer": "16.9.2", + "@types/shallow-equals": "1.0.0", "@typescript-eslint/eslint-plugin": "2.25.0", "@typescript-eslint/parser": "2.25.0", "babel-eslint": "10.1.0", diff --git a/packager/generateModulePaths.js b/packager/generateModulePaths.js index e0e07c768..2c7d0099e 100644 --- a/packager/generateModulePaths.js +++ b/packager/generateModulePaths.js @@ -16,7 +16,7 @@ const modulePaths = moduleNames.map((moduleName) => { if (moduleName.startsWith(localPrefix)) { return `./${moduleName.substring(localPrefix.length)}`; } - if (moduleName.endsWith('.js')) { + if (moduleName.endsWith('.js') || moduleName.endsWith('.ts')) { return `./node_modules/${moduleName}`; } try { diff --git a/packager/moduleNames.js b/packager/moduleNames.js index 7172ac08b..8ec3365d6 100644 --- a/packager/moduleNames.js +++ b/packager/moduleNames.js @@ -3,12 +3,16 @@ module.exports = [ 'app/actions/device/index.js', + 'app/actions/helpers/channels.ts', 'app/actions/navigation/index.js', 'app/actions/views/channel.js', + 'app/actions/views/post.js', 'app/actions/views/login.js', 'app/actions/views/root.js', 'app/actions/views/select_server.js', 'app/actions/views/user.js', + 'app/actions/websocket.ts', + 'app/client/websocket.ts', 'app/constants/deep_linking.js', 'app/constants/device.js', 'app/constants/index.js', @@ -16,6 +20,7 @@ module.exports = [ 'app/constants/navigation.js', 'app/constants/post_textbox.js', 'app/constants/view.js', + 'app/constants/websocket.ts', 'app/i18n/index.js', 'app/init/credentials.js', 'app/init/device.js', @@ -55,6 +60,117 @@ module.exports = [ 'app/reducers/views/team.js', 'app/reducers/views/thread.js', 'app/reducers/views/user.js', + 'app/redux/action_types/bots.ts', + 'app/redux/action_types/channels.ts', + 'app/redux/action_types/emojis.ts', + 'app/redux/action_types/errors.ts', + 'app/redux/action_types/files.ts', + 'app/redux/action_types/general.ts', + 'app/redux/action_types/gifs.ts', + 'app/redux/action_types/groups.ts', + 'app/redux/action_types/index.ts', + 'app/redux/action_types/integrations.ts', + 'app/redux/action_types/jobs.ts', + 'app/redux/action_types/plugins.ts', + 'app/redux/action_types/posts.ts', + 'app/redux/action_types/preferences.ts', + 'app/redux/action_types/roles.ts', + 'app/redux/action_types/schemes.ts', + 'app/redux/action_types/search.ts', + 'app/redux/action_types/teams.ts', + 'app/redux/action_types/users.ts', + 'app/redux/actions/channels.ts', + 'app/redux/actions/emojis.ts', + 'app/redux/actions/errors.ts', + 'app/redux/actions/files.ts', + 'app/redux/actions/general.ts', + 'app/redux/actions/helpers.ts', + 'app/redux/actions/posts.ts', + 'app/redux/actions/preferences.ts', + 'app/redux/actions/roles.ts', + 'app/redux/actions/teams.ts', + 'app/redux/actions/timezone.ts', + 'app/redux/actions/users.ts', + 'app/redux/client/client4.ts', + 'app/redux/client/fetch_etag.ts', + 'app/redux/client/index.ts', + 'app/redux/constants/emoji.ts', + 'app/redux/constants/files.ts', + 'app/redux/constants/general.ts', + 'app/redux/constants/groups.ts', + 'app/redux/constants/index.ts', + 'app/redux/constants/permissions.ts', + 'app/redux/constants/plugins.ts', + 'app/redux/constants/posts.ts', + 'app/redux/constants/preferences.ts', + 'app/redux/constants/request_status.ts', + 'app/redux/constants/stats.ts', + 'app/redux/constants/teams.ts', + 'app/redux/constants/users.ts', + 'app/redux/constants/websocket.ts', + 'app/redux/reducers/entities/bots.ts', + 'app/redux/reducers/entities/channels.ts', + 'app/redux/reducers/entities/emojis.ts', + 'app/redux/reducers/entities/files.ts', + 'app/redux/reducers/entities/general.ts', + 'app/redux/reducers/entities/gifs.ts', + 'app/redux/reducers/entities/groups.ts', + 'app/redux/reducers/entities/index.ts', + 'app/redux/reducers/entities/integrations.ts', + 'app/redux/reducers/entities/jobs.ts', + 'app/redux/reducers/entities/posts.ts', + 'app/redux/reducers/entities/preferences.ts', + 'app/redux/reducers/entities/roles.ts', + 'app/redux/reducers/entities/schemes.ts', + 'app/redux/reducers/entities/search.ts', + 'app/redux/reducers/entities/teams.ts', + 'app/redux/reducers/entities/typing.ts', + 'app/redux/reducers/entities/users.ts', + 'app/redux/reducers/errors/index.ts', + 'app/redux/reducers/index.ts', + 'app/redux/reducers/requests/channels.ts', + 'app/redux/reducers/requests/files.ts', + 'app/redux/reducers/requests/general.ts', + 'app/redux/reducers/requests/helpers.ts', + 'app/redux/reducers/requests/index.ts', + 'app/redux/reducers/requests/jobs.ts', + 'app/redux/reducers/requests/posts.ts', + 'app/redux/reducers/requests/roles.ts', + 'app/redux/reducers/requests/search.ts', + 'app/redux/reducers/requests/teams.ts', + 'app/redux/reducers/requests/users.ts', + 'app/redux/reducers/websocket.ts', + 'app/redux/selectors/entities/channels.ts', + 'app/redux/selectors/entities/common.ts', + 'app/redux/selectors/entities/emojis.ts', + 'app/redux/selectors/entities/general.ts', + 'app/redux/selectors/entities/i18n.ts', + 'app/redux/selectors/entities/posts.ts', + 'app/redux/selectors/entities/preferences.ts', + 'app/redux/selectors/entities/roles.ts', + 'app/redux/selectors/entities/roles_helpers.ts', + 'app/redux/selectors/entities/teams.ts', + 'app/redux/selectors/entities/timezone.ts', + 'app/redux/selectors/entities/users.ts', + 'app/redux/store/configureStore.prod.ts', + 'app/redux/store/helpers.ts', + 'app/redux/store/index.ts', + 'app/redux/store/initial_state.ts', + 'app/redux/store/middleware.ts', + 'app/redux/store/reducer_registry.ts', + 'app/redux/types/actions.ts', + 'app/redux/utils/channel_utils.ts', + 'app/redux/utils/emoji_utils.ts', + 'app/redux/utils/event_emitter.ts', + 'app/redux/utils/file_utils.ts', + 'app/redux/utils/helpers.ts', + 'app/redux/utils/i18n_utils.ts', + 'app/redux/utils/key_mirror.ts', + 'app/redux/utils/post_utils.ts', + 'app/redux/utils/preference_utils.ts', + 'app/redux/utils/sentry.ts', + 'app/redux/utils/team_utils.ts', + 'app/redux/utils/user_utils.ts', 'app/screens/index.js', 'app/selectors/channel.js', 'app/selectors/i18n.js', @@ -392,123 +508,6 @@ module.exports = [ 'node_modules/lodash/union.js', 'node_modules/lodash/uniqueId.js', 'node_modules/lodash/unset.js', - 'node_modules/mattermost-redux/action_types/admin.js', - 'node_modules/mattermost-redux/action_types/alerts.js', - 'node_modules/mattermost-redux/action_types/bots.js', - 'node_modules/mattermost-redux/action_types/channels.js', - 'node_modules/mattermost-redux/action_types/emojis.js', - 'node_modules/mattermost-redux/action_types/errors.js', - 'node_modules/mattermost-redux/action_types/files.js', - 'node_modules/mattermost-redux/action_types/general.js', - 'node_modules/mattermost-redux/action_types/gifs.js', - 'node_modules/mattermost-redux/action_types/groups.js', - 'node_modules/mattermost-redux/action_types/index.js', - 'node_modules/mattermost-redux/action_types/integrations.js', - 'node_modules/mattermost-redux/action_types/jobs.js', - 'node_modules/mattermost-redux/action_types/plugins.js', - 'node_modules/mattermost-redux/action_types/posts.js', - 'node_modules/mattermost-redux/action_types/preferences.js', - 'node_modules/mattermost-redux/action_types/roles.js', - 'node_modules/mattermost-redux/action_types/schemes.js', - 'node_modules/mattermost-redux/action_types/search.js', - 'node_modules/mattermost-redux/action_types/teams.js', - 'node_modules/mattermost-redux/action_types/users.js', - 'node_modules/mattermost-redux/actions/channels.js', - 'node_modules/mattermost-redux/actions/emojis.js', - 'node_modules/mattermost-redux/actions/errors.js', - 'node_modules/mattermost-redux/actions/files.js', - 'node_modules/mattermost-redux/actions/general.js', - 'node_modules/mattermost-redux/actions/helpers.js', - 'node_modules/mattermost-redux/actions/posts.js', - 'node_modules/mattermost-redux/actions/preferences.js', - 'node_modules/mattermost-redux/actions/roles.js', - 'node_modules/mattermost-redux/actions/teams.js', - 'node_modules/mattermost-redux/actions/timezone.js', - 'node_modules/mattermost-redux/actions/users.js', - 'node_modules/mattermost-redux/client/client4.js', - 'node_modules/mattermost-redux/client/fetch_etag.js', - 'node_modules/mattermost-redux/client/index.js', - 'node_modules/mattermost-redux/constants/alerts.js', - 'node_modules/mattermost-redux/constants/emoji.js', - 'node_modules/mattermost-redux/constants/files.js', - 'node_modules/mattermost-redux/constants/general.js', - 'node_modules/mattermost-redux/constants/groups.js', - 'node_modules/mattermost-redux/constants/index.js', - 'node_modules/mattermost-redux/constants/permissions.js', - 'node_modules/mattermost-redux/constants/plugins.js', - 'node_modules/mattermost-redux/constants/posts.js', - 'node_modules/mattermost-redux/constants/preferences.js', - 'node_modules/mattermost-redux/constants/request_status.js', - 'node_modules/mattermost-redux/constants/stats.js', - 'node_modules/mattermost-redux/constants/teams.js', - 'node_modules/mattermost-redux/constants/users.js', - 'node_modules/mattermost-redux/constants/websocket.js', - 'node_modules/mattermost-redux/reducers/entities/admin.js', - 'node_modules/mattermost-redux/reducers/entities/alerts.js', - 'node_modules/mattermost-redux/reducers/entities/bots.js', - 'node_modules/mattermost-redux/reducers/entities/channels.js', - 'node_modules/mattermost-redux/reducers/entities/emojis.js', - 'node_modules/mattermost-redux/reducers/entities/files.js', - 'node_modules/mattermost-redux/reducers/entities/general.js', - 'node_modules/mattermost-redux/reducers/entities/gifs.js', - 'node_modules/mattermost-redux/reducers/entities/groups.js', - 'node_modules/mattermost-redux/reducers/entities/index.js', - 'node_modules/mattermost-redux/reducers/entities/integrations.js', - 'node_modules/mattermost-redux/reducers/entities/jobs.js', - 'node_modules/mattermost-redux/reducers/entities/posts.js', - 'node_modules/mattermost-redux/reducers/entities/preferences.js', - 'node_modules/mattermost-redux/reducers/entities/roles.js', - 'node_modules/mattermost-redux/reducers/entities/schemes.js', - 'node_modules/mattermost-redux/reducers/entities/search.js', - 'node_modules/mattermost-redux/reducers/entities/teams.js', - 'node_modules/mattermost-redux/reducers/entities/typing.js', - 'node_modules/mattermost-redux/reducers/entities/users.js', - 'node_modules/mattermost-redux/reducers/errors/index.js', - 'node_modules/mattermost-redux/reducers/index.js', - 'node_modules/mattermost-redux/reducers/requests/admin.js', - 'node_modules/mattermost-redux/reducers/requests/channels.js', - 'node_modules/mattermost-redux/reducers/requests/files.js', - 'node_modules/mattermost-redux/reducers/requests/general.js', - 'node_modules/mattermost-redux/reducers/requests/helpers.js', - 'node_modules/mattermost-redux/reducers/requests/index.js', - 'node_modules/mattermost-redux/reducers/requests/jobs.js', - 'node_modules/mattermost-redux/reducers/requests/posts.js', - 'node_modules/mattermost-redux/reducers/requests/roles.js', - 'node_modules/mattermost-redux/reducers/requests/search.js', - 'node_modules/mattermost-redux/reducers/requests/teams.js', - 'node_modules/mattermost-redux/reducers/requests/users.js', - 'node_modules/mattermost-redux/reducers/websocket.js', - 'node_modules/mattermost-redux/selectors/entities/channels.js', - 'node_modules/mattermost-redux/selectors/entities/common.js', - 'node_modules/mattermost-redux/selectors/entities/emojis.js', - 'node_modules/mattermost-redux/selectors/entities/general.js', - 'node_modules/mattermost-redux/selectors/entities/i18n.js', - 'node_modules/mattermost-redux/selectors/entities/posts.js', - 'node_modules/mattermost-redux/selectors/entities/preferences.js', - 'node_modules/mattermost-redux/selectors/entities/roles.js', - 'node_modules/mattermost-redux/selectors/entities/roles_helpers.js', - 'node_modules/mattermost-redux/selectors/entities/teams.js', - 'node_modules/mattermost-redux/selectors/entities/timezone.js', - 'node_modules/mattermost-redux/selectors/entities/users.js', - 'node_modules/mattermost-redux/store/configureStore.prod.js', - 'node_modules/mattermost-redux/store/helpers.js', - 'node_modules/mattermost-redux/store/index.js', - 'node_modules/mattermost-redux/store/initial_state.js', - 'node_modules/mattermost-redux/store/middleware.js', - 'node_modules/mattermost-redux/store/reducer_registry.js', - 'node_modules/mattermost-redux/types/actions.js', - 'node_modules/mattermost-redux/utils/channel_utils.js', - 'node_modules/mattermost-redux/utils/emoji_utils.js', - 'node_modules/mattermost-redux/utils/event_emitter.js', - 'node_modules/mattermost-redux/utils/file_utils.js', - 'node_modules/mattermost-redux/utils/helpers.js', - 'node_modules/mattermost-redux/utils/i18n_utils.js', - 'node_modules/mattermost-redux/utils/key_mirror.js', - 'node_modules/mattermost-redux/utils/post_utils.js', - 'node_modules/mattermost-redux/utils/preference_utils.js', - 'node_modules/mattermost-redux/utils/sentry.js', - 'node_modules/mattermost-redux/utils/team_utils.js', - 'node_modules/mattermost-redux/utils/user_utils.js', 'node_modules/metro/src/lib/bundle-modules/HMRClient.js', 'node_modules/metro/src/lib/bundle-modules/WebSocketHMRClient.js', 'node_modules/metro/src/lib/bundle-modules/injectUpdate.js', @@ -535,7 +534,7 @@ module.exports = [ 'node_modules/react-intl/lib/index.js', 'node_modules/react-intl/locale-data/en.js', 'node_modules/react-intl/locale-data/index.js', - 'node_modules/react-is/cjs/react-is.production.min.js', + 'node_modules/react-is/cjs/react-is.production.js', 'node_modules/react-is/index.js', 'node_modules/react-lifecycles-compat/react-lifecycles-compat.cjs.js', 'node_modules/react-native-cookies/index.js', @@ -650,6 +649,9 @@ module.exports = [ 'node_modules/react-native/Libraries/Components/Touchable/PooledClass.js', 'node_modules/react-native/Libraries/Components/Touchable/Position.js', 'node_modules/react-native/Libraries/Components/Touchable/Touchable.js', + 'node_modules/react-native/Libraries/Components/Touchable/TouchableHighlight.js', + 'node_modules/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.android.js', + 'node_modules/react-native/Libraries/Components/Touchable/TouchableOpacity.js', 'node_modules/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js', 'node_modules/react-native/Libraries/Components/Touchable/ensurePositiveDelayProps.js', 'node_modules/react-native/Libraries/Components/View/PlatformViewPropTypes.android.js', @@ -699,6 +701,7 @@ module.exports = [ 'node_modules/react-native/Libraries/Image/AssetRegistry.js', 'node_modules/react-native/Libraries/Image/AssetSourceResolver.js', 'node_modules/react-native/Libraries/Image/Image.android.js', + 'node_modules/react-native/Libraries/Image/ImageBackground.js', 'node_modules/react-native/Libraries/Image/ImageViewNativeComponent.js', 'node_modules/react-native/Libraries/Image/TextInlineImageNativeComponent.js', 'node_modules/react-native/Libraries/Image/assetPathUtils.js', @@ -748,6 +751,7 @@ module.exports = [ 'node_modules/react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore.js', 'node_modules/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js', 'node_modules/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-prod.js', + 'node_modules/react-native/Libraries/Renderer/shims/NativeMethodsMixin.js', 'node_modules/react-native/Libraries/Renderer/shims/ReactNative.js', 'node_modules/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js', 'node_modules/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js', @@ -828,9 +832,9 @@ module.exports = [ 'node_modules/react-redux/lib/utils/verifyPlainObject.js', 'node_modules/react-redux/lib/utils/warning.js', 'node_modules/react-redux/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js', - 'node_modules/react-refresh/cjs/react-refresh-runtime.production.min.js', + 'node_modules/react-refresh/cjs/react-refresh-runtime.production.js', 'node_modules/react-refresh/runtime.js', - 'node_modules/react/cjs/react.production.min.js', + 'node_modules/react/cjs/react.production.js', 'node_modules/react/index.js', 'node_modules/redux-action-buffer/index.js', 'node_modules/redux-batched-actions/lib/index.js', @@ -861,7 +865,6 @@ module.exports = [ 'node_modules/redux-persist/lib/utils/setImmediate.js', 'node_modules/redux-thunk/lib/index.js', 'node_modules/redux/lib/redux.js', - 'node_modules/regenerator-runtime/runtime.js', 'node_modules/requires-port/index.js', 'node_modules/reselect/lib/index.js', 'node_modules/rn-fetch-blob/android.js', @@ -876,8 +879,8 @@ module.exports = [ 'node_modules/sc-errors/decycle.js', 'node_modules/sc-errors/index.js', 'node_modules/sc-formatter/index.js', - 'node_modules/scheduler/cjs/scheduler-tracing.production.min.js', - 'node_modules/scheduler/cjs/scheduler.production.min.js', + 'node_modules/scheduler/cjs/scheduler-tracing.production.js', + 'node_modules/scheduler/cjs/scheduler.production.js', 'node_modules/scheduler/index.js', 'node_modules/scheduler/tracing.js', 'node_modules/semver/classes/comparator.js', @@ -924,7 +927,7 @@ module.exports = [ 'node_modules/semver/ranges/valid.js', 'node_modules/serialize-error/index.js', 'node_modules/shallow-equals/index.js', - 'node_modules/stacktrace-parser/dist/stack-trace-parser.umd.js', + 'node_modules/stacktrace-parser/dist/stack-trace-parser.cjs.js', 'node_modules/symbol-observable/lib/index.js', 'node_modules/symbol-observable/lib/ponyfill.js', 'node_modules/tslib/tslib.js', diff --git a/packager/modulePaths.js b/packager/modulePaths.js index d12902e5d..69d6bb48e 100644 --- a/packager/modulePaths.js +++ b/packager/modulePaths.js @@ -1,14 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. - module.exports = [ './node_modules/app/actions/device/index.js', + './node_modules/app/actions/helpers/channels.ts', './node_modules/app/actions/navigation/index.js', './node_modules/app/actions/views/channel.js', + './node_modules/app/actions/views/post.js', './node_modules/app/actions/views/login.js', './node_modules/app/actions/views/root.js', './node_modules/app/actions/views/select_server.js', './node_modules/app/actions/views/user.js', + './node_modules/app/actions/websocket.ts', + './node_modules/app/client/websocket.ts', './node_modules/app/constants/deep_linking.js', './node_modules/app/constants/device.js', './node_modules/app/constants/index.js', @@ -16,6 +19,7 @@ module.exports = [ './node_modules/app/constants/navigation.js', './node_modules/app/constants/post_textbox.js', './node_modules/app/constants/view.js', + './node_modules/app/constants/websocket.ts', './node_modules/app/i18n/index.js', './node_modules/app/init/credentials.js', './node_modules/app/init/device.js', @@ -55,6 +59,117 @@ module.exports = [ './node_modules/app/reducers/views/team.js', './node_modules/app/reducers/views/thread.js', './node_modules/app/reducers/views/user.js', + './node_modules/app/redux/action_types/bots.ts', + './node_modules/app/redux/action_types/channels.ts', + './node_modules/app/redux/action_types/emojis.ts', + './node_modules/app/redux/action_types/errors.ts', + './node_modules/app/redux/action_types/files.ts', + './node_modules/app/redux/action_types/general.ts', + './node_modules/app/redux/action_types/gifs.ts', + './node_modules/app/redux/action_types/groups.ts', + './node_modules/app/redux/action_types/index.ts', + './node_modules/app/redux/action_types/integrations.ts', + './node_modules/app/redux/action_types/jobs.ts', + './node_modules/app/redux/action_types/plugins.ts', + './node_modules/app/redux/action_types/posts.ts', + './node_modules/app/redux/action_types/preferences.ts', + './node_modules/app/redux/action_types/roles.ts', + './node_modules/app/redux/action_types/schemes.ts', + './node_modules/app/redux/action_types/search.ts', + './node_modules/app/redux/action_types/teams.ts', + './node_modules/app/redux/action_types/users.ts', + './node_modules/app/redux/actions/channels.ts', + './node_modules/app/redux/actions/emojis.ts', + './node_modules/app/redux/actions/errors.ts', + './node_modules/app/redux/actions/files.ts', + './node_modules/app/redux/actions/general.ts', + './node_modules/app/redux/actions/helpers.ts', + './node_modules/app/redux/actions/posts.ts', + './node_modules/app/redux/actions/preferences.ts', + './node_modules/app/redux/actions/roles.ts', + './node_modules/app/redux/actions/teams.ts', + './node_modules/app/redux/actions/timezone.ts', + './node_modules/app/redux/actions/users.ts', + './node_modules/app/redux/client/client4.ts', + './node_modules/app/redux/client/fetch_etag.ts', + './node_modules/app/redux/client/index.ts', + './node_modules/app/redux/constants/emoji.ts', + './node_modules/app/redux/constants/files.ts', + './node_modules/app/redux/constants/general.ts', + './node_modules/app/redux/constants/groups.ts', + './node_modules/app/redux/constants/index.ts', + './node_modules/app/redux/constants/permissions.ts', + './node_modules/app/redux/constants/plugins.ts', + './node_modules/app/redux/constants/posts.ts', + './node_modules/app/redux/constants/preferences.ts', + './node_modules/app/redux/constants/request_status.ts', + './node_modules/app/redux/constants/stats.ts', + './node_modules/app/redux/constants/teams.ts', + './node_modules/app/redux/constants/users.ts', + './node_modules/app/redux/constants/websocket.ts', + './node_modules/app/redux/reducers/entities/bots.ts', + './node_modules/app/redux/reducers/entities/channels.ts', + './node_modules/app/redux/reducers/entities/emojis.ts', + './node_modules/app/redux/reducers/entities/files.ts', + './node_modules/app/redux/reducers/entities/general.ts', + './node_modules/app/redux/reducers/entities/gifs.ts', + './node_modules/app/redux/reducers/entities/groups.ts', + './node_modules/app/redux/reducers/entities/index.ts', + './node_modules/app/redux/reducers/entities/integrations.ts', + './node_modules/app/redux/reducers/entities/jobs.ts', + './node_modules/app/redux/reducers/entities/posts.ts', + './node_modules/app/redux/reducers/entities/preferences.ts', + './node_modules/app/redux/reducers/entities/roles.ts', + './node_modules/app/redux/reducers/entities/schemes.ts', + './node_modules/app/redux/reducers/entities/search.ts', + './node_modules/app/redux/reducers/entities/teams.ts', + './node_modules/app/redux/reducers/entities/typing.ts', + './node_modules/app/redux/reducers/entities/users.ts', + './node_modules/app/redux/reducers/errors/index.ts', + './node_modules/app/redux/reducers/index.ts', + './node_modules/app/redux/reducers/requests/channels.ts', + './node_modules/app/redux/reducers/requests/files.ts', + './node_modules/app/redux/reducers/requests/general.ts', + './node_modules/app/redux/reducers/requests/helpers.ts', + './node_modules/app/redux/reducers/requests/index.ts', + './node_modules/app/redux/reducers/requests/jobs.ts', + './node_modules/app/redux/reducers/requests/posts.ts', + './node_modules/app/redux/reducers/requests/roles.ts', + './node_modules/app/redux/reducers/requests/search.ts', + './node_modules/app/redux/reducers/requests/teams.ts', + './node_modules/app/redux/reducers/requests/users.ts', + './node_modules/app/redux/reducers/websocket.ts', + './node_modules/app/redux/selectors/entities/channels.ts', + './node_modules/app/redux/selectors/entities/common.ts', + './node_modules/app/redux/selectors/entities/emojis.ts', + './node_modules/app/redux/selectors/entities/general.ts', + './node_modules/app/redux/selectors/entities/i18n.ts', + './node_modules/app/redux/selectors/entities/posts.ts', + './node_modules/app/redux/selectors/entities/preferences.ts', + './node_modules/app/redux/selectors/entities/roles.ts', + './node_modules/app/redux/selectors/entities/roles_helpers.ts', + './node_modules/app/redux/selectors/entities/teams.ts', + './node_modules/app/redux/selectors/entities/timezone.ts', + './node_modules/app/redux/selectors/entities/users.ts', + './node_modules/app/redux/store/configureStore.prod.ts', + './node_modules/app/redux/store/helpers.ts', + './node_modules/app/redux/store/index.ts', + './node_modules/app/redux/store/initial_state.ts', + './node_modules/app/redux/store/middleware.ts', + './node_modules/app/redux/store/reducer_registry.ts', + './node_modules/app/redux/types/actions.ts', + './node_modules/app/redux/utils/channel_utils.ts', + './node_modules/app/redux/utils/emoji_utils.ts', + './node_modules/app/redux/utils/event_emitter.ts', + './node_modules/app/redux/utils/file_utils.ts', + './node_modules/app/redux/utils/helpers.ts', + './node_modules/app/redux/utils/i18n_utils.ts', + './node_modules/app/redux/utils/key_mirror.ts', + './node_modules/app/redux/utils/post_utils.ts', + './node_modules/app/redux/utils/preference_utils.ts', + './node_modules/app/redux/utils/sentry.ts', + './node_modules/app/redux/utils/team_utils.ts', + './node_modules/app/redux/utils/user_utils.ts', './node_modules/app/screens/index.js', './node_modules/app/selectors/channel.js', './node_modules/app/selectors/i18n.js', @@ -112,6 +227,15 @@ module.exports = [ './node_modules/node_modules/@react-native-community/async-storage/lib/AsyncStorage.js', './node_modules/node_modules/@react-native-community/async-storage/lib/hooks.js', './node_modules/node_modules/@react-native-community/async-storage/lib/index.js', + './node_modules/node_modules/@react-native-community/netinfo/src/index.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/deprecatedState.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/deprecatedTypes.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/deprecatedUtils.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/internetReachability.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/nativeInterface.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/state.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/types.ts', + './node_modules/node_modules/@react-native-community/netinfo/src/internal/utils.ts', './node_modules/node_modules/base-64/base64.js', './node_modules/node_modules/base64-js/index.js', './node_modules/node_modules/buffer/index.js', @@ -381,123 +505,6 @@ module.exports = [ './node_modules/node_modules/lodash/union.js', './node_modules/node_modules/lodash/uniqueId.js', './node_modules/node_modules/lodash/unset.js', - './node_modules/node_modules/mattermost-redux/action_types/admin.js', - './node_modules/node_modules/mattermost-redux/action_types/alerts.js', - './node_modules/node_modules/mattermost-redux/action_types/bots.js', - './node_modules/node_modules/mattermost-redux/action_types/channels.js', - './node_modules/node_modules/mattermost-redux/action_types/emojis.js', - './node_modules/node_modules/mattermost-redux/action_types/errors.js', - './node_modules/node_modules/mattermost-redux/action_types/files.js', - './node_modules/node_modules/mattermost-redux/action_types/general.js', - './node_modules/node_modules/mattermost-redux/action_types/gifs.js', - './node_modules/node_modules/mattermost-redux/action_types/groups.js', - './node_modules/node_modules/mattermost-redux/action_types/index.js', - './node_modules/node_modules/mattermost-redux/action_types/integrations.js', - './node_modules/node_modules/mattermost-redux/action_types/jobs.js', - './node_modules/node_modules/mattermost-redux/action_types/plugins.js', - './node_modules/node_modules/mattermost-redux/action_types/posts.js', - './node_modules/node_modules/mattermost-redux/action_types/preferences.js', - './node_modules/node_modules/mattermost-redux/action_types/roles.js', - './node_modules/node_modules/mattermost-redux/action_types/schemes.js', - './node_modules/node_modules/mattermost-redux/action_types/search.js', - './node_modules/node_modules/mattermost-redux/action_types/teams.js', - './node_modules/node_modules/mattermost-redux/action_types/users.js', - './node_modules/node_modules/mattermost-redux/actions/channels.js', - './node_modules/node_modules/mattermost-redux/actions/emojis.js', - './node_modules/node_modules/mattermost-redux/actions/errors.js', - './node_modules/node_modules/mattermost-redux/actions/files.js', - './node_modules/node_modules/mattermost-redux/actions/general.js', - './node_modules/node_modules/mattermost-redux/actions/helpers.js', - './node_modules/node_modules/mattermost-redux/actions/posts.js', - './node_modules/node_modules/mattermost-redux/actions/preferences.js', - './node_modules/node_modules/mattermost-redux/actions/roles.js', - './node_modules/node_modules/mattermost-redux/actions/teams.js', - './node_modules/node_modules/mattermost-redux/actions/timezone.js', - './node_modules/node_modules/mattermost-redux/actions/users.js', - './node_modules/node_modules/mattermost-redux/client/client4.js', - './node_modules/node_modules/mattermost-redux/client/fetch_etag.js', - './node_modules/node_modules/mattermost-redux/client/index.js', - './node_modules/node_modules/mattermost-redux/constants/alerts.js', - './node_modules/node_modules/mattermost-redux/constants/emoji.js', - './node_modules/node_modules/mattermost-redux/constants/files.js', - './node_modules/node_modules/mattermost-redux/constants/general.js', - './node_modules/node_modules/mattermost-redux/constants/groups.js', - './node_modules/node_modules/mattermost-redux/constants/index.js', - './node_modules/node_modules/mattermost-redux/constants/permissions.js', - './node_modules/node_modules/mattermost-redux/constants/plugins.js', - './node_modules/node_modules/mattermost-redux/constants/posts.js', - './node_modules/node_modules/mattermost-redux/constants/preferences.js', - './node_modules/node_modules/mattermost-redux/constants/request_status.js', - './node_modules/node_modules/mattermost-redux/constants/stats.js', - './node_modules/node_modules/mattermost-redux/constants/teams.js', - './node_modules/node_modules/mattermost-redux/constants/users.js', - './node_modules/node_modules/mattermost-redux/constants/websocket.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/admin.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/alerts.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/bots.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/channels.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/emojis.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/files.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/general.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/gifs.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/groups.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/index.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/integrations.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/jobs.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/posts.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/preferences.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/roles.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/schemes.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/search.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/teams.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/typing.js', - './node_modules/node_modules/mattermost-redux/reducers/entities/users.js', - './node_modules/node_modules/mattermost-redux/reducers/errors/index.js', - './node_modules/node_modules/mattermost-redux/reducers/index.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/admin.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/channels.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/files.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/general.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/helpers.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/index.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/jobs.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/posts.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/roles.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/search.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/teams.js', - './node_modules/node_modules/mattermost-redux/reducers/requests/users.js', - './node_modules/node_modules/mattermost-redux/reducers/websocket.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/channels.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/common.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/emojis.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/general.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/i18n.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/posts.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/preferences.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/roles.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/roles_helpers.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/teams.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/timezone.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/users.js', - './node_modules/node_modules/mattermost-redux/store/configureStore.prod.js', - './node_modules/node_modules/mattermost-redux/store/helpers.js', - './node_modules/node_modules/mattermost-redux/store/index.js', - './node_modules/node_modules/mattermost-redux/store/initial_state.js', - './node_modules/node_modules/mattermost-redux/store/middleware.js', - './node_modules/node_modules/mattermost-redux/store/reducer_registry.js', - './node_modules/node_modules/mattermost-redux/types/actions.js', - './node_modules/node_modules/mattermost-redux/utils/channel_utils.js', - './node_modules/node_modules/mattermost-redux/utils/emoji_utils.js', - './node_modules/node_modules/mattermost-redux/utils/event_emitter.js', - './node_modules/node_modules/mattermost-redux/utils/file_utils.js', - './node_modules/node_modules/mattermost-redux/utils/helpers.js', - './node_modules/node_modules/mattermost-redux/utils/i18n_utils.js', - './node_modules/node_modules/mattermost-redux/utils/key_mirror.js', - './node_modules/node_modules/mattermost-redux/utils/post_utils.js', - './node_modules/node_modules/mattermost-redux/utils/preference_utils.js', - './node_modules/node_modules/mattermost-redux/utils/sentry.js', - './node_modules/node_modules/mattermost-redux/utils/team_utils.js', - './node_modules/node_modules/mattermost-redux/utils/user_utils.js', './node_modules/node_modules/metro/src/lib/bundle-modules/HMRClient.js', './node_modules/node_modules/metro/src/lib/bundle-modules/WebSocketHMRClient.js', './node_modules/node_modules/metro/src/lib/bundle-modules/injectUpdate.js', @@ -638,6 +645,9 @@ module.exports = [ './node_modules/node_modules/react-native/Libraries/Components/Touchable/PooledClass.js', './node_modules/node_modules/react-native/Libraries/Components/Touchable/Position.js', './node_modules/node_modules/react-native/Libraries/Components/Touchable/Touchable.js', + './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableHighlight.js', + './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.android.js', + './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableOpacity.js', './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js', './node_modules/node_modules/react-native/Libraries/Components/Touchable/ensurePositiveDelayProps.js', './node_modules/node_modules/react-native/Libraries/Components/View/PlatformViewPropTypes.android.js', @@ -849,7 +859,6 @@ module.exports = [ './node_modules/node_modules/redux-persist/lib/utils/setImmediate.js', './node_modules/node_modules/redux-thunk/lib/index.js', './node_modules/node_modules/redux/lib/redux.js', - './node_modules/node_modules/regenerator-runtime/runtime.js', './node_modules/node_modules/requires-port/index.js', './node_modules/node_modules/reselect/lib/index.js', './node_modules/node_modules/rn-fetch-blob/android.js', @@ -912,7 +921,7 @@ module.exports = [ './node_modules/node_modules/semver/ranges/valid.js', './node_modules/node_modules/serialize-error/index.js', './node_modules/node_modules/shallow-equals/index.js', - './node_modules/node_modules/stacktrace-parser/dist/stack-trace-parser.umd.js', + './node_modules/node_modules/stacktrace-parser/dist/stack-trace-parser.cjs.js', './node_modules/node_modules/symbol-observable/lib/index.js', './node_modules/node_modules/symbol-observable/lib/ponyfill.js', './node_modules/node_modules/tslib/tslib.js', diff --git a/share_extension/android/actions/index.js b/share_extension/android/actions/index.js index 0d182cf36..a3f9748d7 100644 --- a/share_extension/android/actions/index.js +++ b/share_extension/android/actions/index.js @@ -1,9 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels'; -import {getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from 'mattermost-redux/selectors/entities/channels'; -import {getChannelByName} from 'mattermost-redux/utils/channel_utils'; +import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels'; +import {getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels'; +import {getChannelByName} from '@mm-redux/utils/channel_utils'; import {loadChannelsForTeam} from 'app/actions/views/channel'; import {ViewTypes} from 'app/constants'; diff --git a/share_extension/android/extension_channels/extension_channels.js b/share_extension/android/extension_channels/extension_channels.js index f69481853..af9528332 100644 --- a/share_extension/android/extension_channels/extension_channels.js +++ b/share_extension/android/extension_channels/extension_channels.js @@ -12,7 +12,7 @@ import { View, } from 'react-native'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import SearchBar from 'app/components/search_bar'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/share_extension/android/extension_post/channel_button/index.js b/share_extension/android/extension_post/channel_button/index.js index be7b012e8..8b844f739 100644 --- a/share_extension/android/extension_post/channel_button/index.js +++ b/share_extension/android/extension_post/channel_button/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {getChannel} from '@mm-redux/selectors/entities/channels'; import ChannelButton from './channel_button'; diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index 04f91bbd3..9252bb904 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -22,9 +22,9 @@ import Video from 'react-native-video'; import LocalAuth from 'react-native-local-auth'; import RNFetchBlob from 'rn-fetch-blob'; -import {Client4} from 'mattermost-redux/client'; -import {Preferences} from 'mattermost-redux/constants'; -import {getFormattedFileSize, lookupMimeType} from 'mattermost-redux/utils/file_utils'; +import {Client4} from '@mm-redux/client'; +import {Preferences} from '@mm-redux/constants'; +import {getFormattedFileSize, lookupMimeType} from '@mm-redux/utils/file_utils'; import Loading from 'app/components/loading'; import PaperPlane from 'app/components/paper_plane'; diff --git a/share_extension/android/extension_post/index.js b/share_extension/android/extension_post/index.js index 08d3504f3..c24c42d63 100644 --- a/share_extension/android/extension_post/index.js +++ b/share_extension/android/extension_post/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getAllChannels, getCurrentChannel, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getAllChannels, getCurrentChannel, getDefaultChannel} from '@mm-redux/selectors/entities/channels'; +import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; import {getTeamChannels} from 'share_extension/android/actions'; import {getAllowedServerMaxFileSize} from 'app/utils/file'; diff --git a/share_extension/android/extension_post/team_button/index.js b/share_extension/android/extension_post/team_button/index.js index fba9d0199..73c4c66bc 100644 --- a/share_extension/android/extension_post/team_button/index.js +++ b/share_extension/android/extension_post/team_button/index.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; -import {getTeam} from 'mattermost-redux/selectors/entities/teams'; +import {getTeam} from '@mm-redux/selectors/entities/teams'; import TeamButton from './team_button'; diff --git a/share_extension/android/extension_teams/extension_teams.js b/share_extension/android/extension_teams/extension_teams.js index 7ff5c1bd7..02776bcba 100644 --- a/share_extension/android/extension_teams/extension_teams.js +++ b/share_extension/android/extension_teams/extension_teams.js @@ -11,7 +11,7 @@ import { View, } from 'react-native'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; diff --git a/share_extension/android/extension_teams/index.js b/share_extension/android/extension_teams/index.js index 0675e7575..1a6ce1bab 100644 --- a/share_extension/android/extension_teams/index.js +++ b/share_extension/android/extension_teams/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams'; +import {getMySortedTeamIds} from '@mm-redux/selectors/entities/teams'; import {extensionSelectTeamId, getTeamChannels} from 'share_extension/android/actions'; diff --git a/share_extension/android/extension_teams/team_item/index.js b/share_extension/android/extension_teams/team_item/index.js index 2cc20446b..aa4cda5a1 100644 --- a/share_extension/android/extension_teams/team_item/index.js +++ b/share_extension/android/extension_teams/team_item/index.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {getTeam} from 'mattermost-redux/selectors/entities/teams'; +import {getTeam} from '@mm-redux/selectors/entities/teams'; import TeamItem from './team_item'; diff --git a/share_extension/android/navigation.js b/share_extension/android/navigation.js index 8431ee017..0085df656 100644 --- a/share_extension/android/navigation.js +++ b/share_extension/android/navigation.js @@ -4,7 +4,7 @@ import {createAppContainer} from 'react-navigation'; import {createStackNavigator} from 'react-navigation-stack'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import ExtensionChannels from './extension_channels'; import ExtensionPost from './extension_post'; diff --git a/share_extension/common/icons/channel_type.js b/share_extension/common/icons/channel_type.js index bc44bc31c..fb726f6d9 100644 --- a/share_extension/common/icons/channel_type.js +++ b/share_extension/common/icons/channel_type.js @@ -7,7 +7,7 @@ import { } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; -import {Preferences} from 'mattermost-redux/constants'; +import {Preferences} from '@mm-redux/constants'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; const defaultTheme = Preferences.THEMES.default; diff --git a/share_extension/common/selectors/index.js b/share_extension/common/selectors/index.js index 71cda64b7..f9c48c34f 100644 --- a/share_extension/common/selectors/index.js +++ b/share_extension/common/selectors/index.js @@ -3,17 +3,17 @@ import {createSelector} from 'reselect'; -import {General} from 'mattermost-redux/constants'; -import {getAllChannels, getChannelsInTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUser, getUsers} from 'mattermost-redux/selectors/entities/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getLastPostPerChannel} from 'mattermost-redux/selectors/entities/posts'; +import {General} from '@mm-redux/constants'; +import {getAllChannels, getChannelsInTeam, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; +import {getCurrentUser, getUsers} from '@mm-redux/selectors/entities/users'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {getLastPostPerChannel} from '@mm-redux/selectors/entities/posts'; import { getMyPreferences, getTeammateNameDisplaySetting, getVisibleTeammate, getVisibleGroupIds, -} from 'mattermost-redux/selectors/entities/preferences'; +} from '@mm-redux/selectors/entities/preferences'; import { completeDirectChannelDisplayName, @@ -22,8 +22,8 @@ import { getUserIdFromChannelName, isAutoClosed, sortChannelsByDisplayName, -} from 'mattermost-redux/utils/channel_utils'; -import {createIdsSelector} from 'mattermost-redux/utils/helpers'; +} from '@mm-redux/utils/channel_utils'; +import {createIdsSelector} from '@mm-redux/utils/helpers'; export const getChannelIdsForExtensionTeam = createIdsSelector( (state) => state.views.extension.selectedTeamId, diff --git a/test/assets/images/test.png b/test/assets/images/test.png new file mode 100644 index 000000000..13ede8580 Binary files /dev/null and b/test/assets/images/test.png differ diff --git a/test/merge_objects.js b/test/merge_objects.js new file mode 100644 index 000000000..c1a3707b4 --- /dev/null +++ b/test/merge_objects.js @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +function isObject(obj) { + return obj && typeof obj === 'object' && !Array.isArray(obj); +} + +// Returns the result of merging two objects. If a field is specified in both a and b, the value from b takes precedence +// unless both values are objects in which case mergeObjects will be called recursively. +export default function mergeObjects(a, b, path = '.') { + if (a === null || a === undefined) { + return b; + } else if (b === null || b === undefined) { + return a; + } + + let result; + + if (isObject(a) && isObject(b)) { + result = {}; + + for (const key of Object.keys(a)) { + result[key] = mergeObjects(a[key], b[key], path + '.' + key); + } + + for (const key of Object.keys(b)) { + if (result.hasOwnProperty(key)) { + continue; + } + + result[key] = b[key]; + } + } else if (isObject(a) || isObject(b)) { + throw new Error(`Mismatched types: ${path} is an object from one source but not the other`); + } else { + result = b; + } + + return result; +} diff --git a/test/setup.js b/test/setup.js index 78e6b3e70..107437666 100644 --- a/test/setup.js +++ b/test/setup.js @@ -6,6 +6,8 @@ import MockAsyncStorage from 'mock-async-storage'; import {configure} from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; +require('isomorphic-fetch'); + configure({adapter: new Adapter()}); const mockImpl = new MockAsyncStorage(); diff --git a/test/test_helper.js b/test/test_helper.js index b85cef79f..334e709e5 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -6,18 +6,26 @@ import nock from 'nock'; import Config from 'assets/config.json'; -import Client from 'mattermost-redux/client/client4'; +import Client from '@mm-redux/client/client4'; + +import {DEFAULT_LOCALE} from '@mm-redux/constants/general'; +import {generateId} from '@mm-redux/utils/helpers'; const PASSWORD = 'password1'; class TestHelper { constructor() { this.basicClient = null; + this.basicClient4 = null; this.basicUser = null; this.basicTeam = null; + this.basicTeamMember = null; this.basicChannel = null; + this.basicChannelMember = null; this.basicPost = null; + this.basicRoles = null; + this.basicScheme = null; } activateMocking() { @@ -31,10 +39,14 @@ class TestHelper { assert(data.status === 'OK'); }; + generateId = () => { + return generateId(); + }; + createClient = () => { const client = new Client(); - client.setUrl(Config.DefaultServerUrl); + client.setUrl(Config.DefaultServerUrl || Config.TestServerUrl); return client; }; @@ -47,6 +59,9 @@ class TestHelper { team_id: teamId, display_name: `Unit Test ${name}`, type: 'O', + delete_at: 0, + total_msg_count: 0, + scheme_id: this.generateId(), }; }; @@ -60,12 +75,29 @@ class TestHelper { }; }; + fakeDmChannel = (userId, otherUserId) => { + return { + name: userId > otherUserId ? otherUserId + '__' + userId : userId + '__' + otherUserId, + team_id: '', + display_name: `${otherUserId}`, + type: 'D', + status: 'offline', + teammate_id: `${otherUserId}`, + id: this.generateId(), + delete_at: 0, + }; + } + fakeChannelMember = (userId, channelId) => { return { user_id: userId, channel_id: channelId, notify_props: {}, roles: 'system_user', + msg_count: 0, + mention_count: 0, + scheme_user: false, + scheme_admin: false, }; }; @@ -82,6 +114,7 @@ class TestHelper { create_at: time, update_at: time, message: `Unit Test ${this.generateId()}`, + type: '', }; }; @@ -109,6 +142,7 @@ class TestHelper { email: this.fakeEmail(), allowed_domains: '', invite_id: inviteId, + scheme_id: this.generateId(), }; }; @@ -138,7 +172,12 @@ class TestHelper { email: this.fakeEmail(), allow_marketing: true, password: PASSWORD, + locale: DEFAULT_LOCALE, username: this.generateId(), + first_name: this.generateId(), + last_name: this.generateId(), + create_at: Date.now(), + delete_at: 0, roles: 'system_user', }; }; @@ -153,6 +192,61 @@ class TestHelper { }; }; + fakeOutgoingHook = (teamId) => { + return { + team_id: teamId, + }; + }; + + fakeOutgoingHookWithId = (teamId) => { + return { + ...this.fakeOutgoingHook(teamId), + id: this.generateId(), + }; + }; + + fakeFiles = (count) => { + const files = []; + while (files.length < count) { + files.push({ + id: this.generateId(), + }); + } + + return files; + }; + + fakeOAuthApp = () => { + return { + name: this.generateId(), + callback_urls: ['http://localhost/notrealurl'], + homepage: 'http://localhost/notrealurl', + description: 'fake app', + is_trusted: false, + icon_url: 'http://localhost/notrealurl', + update_at: 1507841118796, + }; + }; + + fakeOAuthAppWithId = () => { + return { + ...this.fakeOAuthApp(), + id: this.generateId(), + }; + }; + + fakeBot = () => { + return { + user_id: this.generateId(), + username: this.generateId(), + display_name: 'Fake bot', + owner_id: this.generateId(), + create_at: 1507840900004, + update_at: 1507840900004, + delete_at: 0, + }; + } + generateId = () => { // Implementation taken from http://stackoverflow.com/a/2117523 let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; @@ -173,6 +267,28 @@ class TestHelper { return 'uid' + id; }; + mockLogin = () => { + nock(this.basicClient4.getBaseRoute()). + post('/users/login'). + reply(200, this.basicUser, {'X-Version-Id': 'Server Version'}); + + nock(this.basicClient4.getBaseRoute()). + get('/users/me/teams/members'). + reply(200, [this.basicTeamMember]); + + nock(this.basicClient4.getBaseRoute()). + get('/users/me/teams/unread'). + reply(200, [{team_id: this.basicTeam.id, msg_count: 0, mention_count: 0}]); + + nock(this.basicClient4.getBaseRoute()). + get('/users/me/teams'). + reply(200, [this.basicTeam]); + + nock(this.basicClient4.getBaseRoute()). + get('/users/me/preferences'). + reply(200, [{user_id: this.basicUser.id, category: 'tutorial_step', name: this.basicUser.id, value: '999'}]); + } + initMockEntities = () => { this.basicUser = this.fakeUserWithId(); this.basicUser.roles = 'system_user system_admin'; @@ -255,12 +371,14 @@ class TestHelper { initBasic = async (client = this.createClient()) => { client.setUrl(Config.TestServerUrl || Config.DefaultServerUrl); this.basicClient = client; + this.basicClient4 = client; this.initMockEntities(); this.activateMocking(); return { client: this.basicClient, + client4: this.basicClient4, user: this.basicUser, team: this.basicTeam, channel: this.basicChannel, @@ -288,9 +406,63 @@ class TestHelper { }; }; + testIncomingHook = () => { + return { + id: this.generateId(), + create_at: 1507840900004, + update_at: 1507840900004, + delete_at: 0, + user_id: this.basicUser.id, + channel_id: this.basicChannel.id, + team_id: this.basicTeam.id, + display_name: 'test', + description: 'test', + }; + }; + + testOutgoingHook = () => { + return { + id: this.generateId(), + token: this.generateId(), + create_at: 1507841118796, + update_at: 1507841118796, + delete_at: 0, + creator_id: this.basicUser.id, + channel_id: this.basicChannel.id, + team_id: this.basicTeam.id, + trigger_words: ['testword'], + trigger_when: 0, + callback_urls: ['http://localhost/notarealendpoint'], + display_name: 'test', + description: '', + content_type: 'application/x-www-form-urlencoded', + }; + } + + testCommand = (teamId) => { + return { + trigger: this.generateId(), + method: 'P', + create_at: 1507841118796, + update_at: 1507841118796, + delete_at: 0, + creator_id: this.basicUser.id, + team_id: teamId, + username: 'test', + icon_url: 'http://localhost/notarealendpoint', + auto_complete: true, + auto_complete_desc: 'test', + auto_complete_hint: 'test', + display_name: 'test', + description: 'test', + url: 'http://localhost/notarealendpoint', + }; + }; + tearDown = async () => { nock.restore(); + this.basicClient = null; this.basicClient4 = null; this.basicUser = null; this.basicTeam = null; diff --git a/test/test_store.js b/test/test_store.js index 3986be3ca..4f53732fa 100644 --- a/test/test_store.js +++ b/test/test_store.js @@ -3,7 +3,7 @@ import {AsyncNodeStorage} from 'redux-persist-node-storage'; import {createTransform, persistStore} from 'redux-persist'; -import configureStore from 'mattermost-redux/store'; +import configureStore from '@mm-redux/store'; export default async function testConfigureStore(preloadedState) { const storageTransform = createTransform( diff --git a/tsconfig.json b/tsconfig.json index 720d45b6c..efc32a06b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,7 @@ "@selectors/*": ["app/selectors/*"], "@telemetry/*": ["/app/telemetry/*"], "@utils/*": ["app/utils/*"], + "@mm-redux/*": ["app/mm-redux/*"], "@websocket": ["app/client/websocket"], "*": ["./*", "node_modules/*"] }