[MM-57384] Investigate app performance on repeated calls to users/status/ids and users/ids calls on posted event (#26644)
https://mattermost.atlassian.net/browse/MM-57384 https://mattermost.atlassian.net/browse/MM-58109 https://mattermost.atlassian.net/browse/MM-58110
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b00b68920d
Коммит
273a999167
@@ -97,6 +97,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
|||||||
|
|
||||||
props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus)
|
props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus)
|
||||||
props["DisableWakeUpReconnectHandler"] = strconv.FormatBool(*c.ExperimentalSettings.DisableWakeUpReconnectHandler)
|
props["DisableWakeUpReconnectHandler"] = strconv.FormatBool(*c.ExperimentalSettings.DisableWakeUpReconnectHandler)
|
||||||
|
props["UsersStatusAndProfileFetchingPollIntervalMilliseconds"] = strconv.FormatInt(*c.ExperimentalSettings.UsersStatusAndProfileFetchingPollIntervalMilliseconds, 10)
|
||||||
|
|
||||||
// Set default values for all options that require a license.
|
// Set default values for all options that require a license.
|
||||||
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ const (
|
|||||||
NativeappSettingsDefaultIosAppDownloadLink = "https://mattermost.com/pl/ios-app/"
|
NativeappSettingsDefaultIosAppDownloadLink = "https://mattermost.com/pl/ios-app/"
|
||||||
|
|
||||||
ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000
|
ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000
|
||||||
|
ExperimentalSettingsDefaultUsersStatusAndProfileFetchingPollIntervalMilliseconds = 3000
|
||||||
|
|
||||||
AnalyticsSettingsDefaultMaxUsersForStatistics = 2500
|
AnalyticsSettingsDefaultMaxUsersForStatistics = 2500
|
||||||
|
|
||||||
@@ -1020,6 +1021,7 @@ type ExperimentalSettings struct {
|
|||||||
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
||||||
DelayChannelAutocomplete *bool `access:"experimental_features"`
|
DelayChannelAutocomplete *bool `access:"experimental_features"`
|
||||||
DisableWakeUpReconnectHandler *bool `access:"experimental_features"`
|
DisableWakeUpReconnectHandler *bool `access:"experimental_features"`
|
||||||
|
UsersStatusAndProfileFetchingPollIntervalMilliseconds *int64 `access:"experimental_features"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExperimentalSettings) SetDefaults() {
|
func (s *ExperimentalSettings) SetDefaults() {
|
||||||
@@ -1062,6 +1064,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
|||||||
if s.DisableWakeUpReconnectHandler == nil {
|
if s.DisableWakeUpReconnectHandler == nil {
|
||||||
s.DisableWakeUpReconnectHandler = NewBool(false)
|
s.DisableWakeUpReconnectHandler = NewBool(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.UsersStatusAndProfileFetchingPollIntervalMilliseconds == nil {
|
||||||
|
s.UsersStatusAndProfileFetchingPollIntervalMilliseconds = NewInt64(ExperimentalSettingsDefaultUsersStatusAndProfileFetchingPollIntervalMilliseconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsSettings struct {
|
type AnalyticsSettings struct {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|||||||
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {addUserIdsForStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import {getStatusesByIds} from 'mattermost-redux/actions/users';
|
import {getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||||
import {Preferences} from 'mattermost-redux/constants';
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
@@ -18,6 +19,12 @@ jest.mock('mattermost-redux/actions/users', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('mattermost-redux/actions/status_profile_polling', () => ({
|
||||||
|
addUserIdsForStatusAndProfileFetchingPoll: jest.fn(() => {
|
||||||
|
return {type: ''};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
interface CustomMatchers<R = unknown> {
|
interface CustomMatchers<R = unknown> {
|
||||||
arrayContainingExactly(stringArray: string[]): R;
|
arrayContainingExactly(stringArray: string[]): R;
|
||||||
}
|
}
|
||||||
@@ -76,20 +83,21 @@ describe('actions/status_actions', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('loadStatusesForChannelAndSidebar', () => {
|
describe('addUserIdsForStatusAndProfileFetchingPoll', () => {
|
||||||
test('load statuses with posts in channel and user in sidebar', () => {
|
test('load statuses with posts in channel and user in sidebar', () => {
|
||||||
const state = cloneDeep(initialState);
|
const state = cloneDeep(initialState);
|
||||||
const testStore = mockStore(state);
|
const testStore = mockStore(state);
|
||||||
testStore.dispatch(Actions.loadStatusesForChannelAndSidebar());
|
testStore.dispatch(Actions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||||
expect(getStatusesByIds).toHaveBeenCalledWith((expect as GreatExpectations).arrayContainingExactly(['current_user_id', 'user_id2', 'user_id3']));
|
expect(addUserIdsForStatusAndProfileFetchingPoll).toHaveBeenCalled();
|
||||||
|
expect(addUserIdsForStatusAndProfileFetchingPoll).toHaveBeenCalledWith({userIdsForStatus: ['user_id2', 'user_id3']});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('load statuses with empty channel and user in sidebar', () => {
|
test('load statuses with empty channel and user in sidebar', () => {
|
||||||
const state = cloneDeep(initialState);
|
const state = cloneDeep(initialState);
|
||||||
state.entities.channels.currentChannelId = 'channel_id2';
|
state.entities.channels.currentChannelId = 'channel_id2';
|
||||||
const testStore = mockStore(state);
|
const testStore = mockStore(state);
|
||||||
testStore.dispatch(Actions.loadStatusesForChannelAndSidebar());
|
testStore.dispatch(Actions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||||
expect(getStatusesByIds).toHaveBeenCalledWith((expect as GreatExpectations).arrayContainingExactly(['current_user_id', 'user_id3']));
|
expect(addUserIdsForStatusAndProfileFetchingPoll).toHaveBeenCalledWith({userIdsForStatus: ['user_id3']});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('load statuses with empty channel and no users in sidebar', () => {
|
test('load statuses with empty channel and no users in sidebar', () => {
|
||||||
@@ -97,8 +105,8 @@ describe('actions/status_actions', () => {
|
|||||||
state.entities.channels.currentChannelId = 'channel_id2';
|
state.entities.channels.currentChannelId = 'channel_id2';
|
||||||
state.entities.preferences.myPreferences = {};
|
state.entities.preferences.myPreferences = {};
|
||||||
const testStore = mockStore(state);
|
const testStore = mockStore(state);
|
||||||
testStore.dispatch(Actions.loadStatusesForChannelAndSidebar());
|
testStore.dispatch(Actions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||||
expect(getStatusesByIds).toHaveBeenCalledWith((expect as GreatExpectations).arrayContainingExactly(['current_user_id']));
|
expect(addUserIdsForStatusAndProfileFetchingPoll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {addUserIdsForStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import {getStatusesByIds} from 'mattermost-redux/actions/users';
|
import {getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
||||||
@@ -15,36 +16,44 @@ import {loadCustomEmojisForCustomStatusesByUserIds} from 'actions/emoji_actions'
|
|||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
export function loadStatusesForChannelAndSidebar(): ActionFunc<boolean, GlobalState> {
|
/**
|
||||||
|
* Adds all the visible users of the current channel i.e users who have recently posted in the current channel
|
||||||
|
* and users who have DMs open with the current user to the status pool for fetching their statuses.
|
||||||
|
*/
|
||||||
|
export function addVisibleUsersInCurrentChannelToStatusPoll(): ActionFunc<boolean, GlobalState> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const currentUserId = getCurrentUserId(state);
|
||||||
const channelId = getCurrentChannelId(state);
|
const currentChannelId = getCurrentChannelId(state);
|
||||||
const postsInChannel = getPostsInCurrentChannel(state);
|
const postsInChannel = getPostsInCurrentChannel(state);
|
||||||
|
const numberOfPostsVisibleInCurrentChannel = state.views.channel.postVisibility[currentChannelId] || 0;
|
||||||
|
|
||||||
const userIds = new Set<string>();
|
const userIdsToFetchStatusFor = new Set<string>();
|
||||||
|
|
||||||
if (postsInChannel) {
|
// We fetch for users who have recently posted in the current channel
|
||||||
const posts = postsInChannel.slice(0, state.views.channel.postVisibility[channelId] || 0);
|
if (postsInChannel && numberOfPostsVisibleInCurrentChannel > 0) {
|
||||||
|
const posts = postsInChannel.slice(0, numberOfPostsVisibleInCurrentChannel);
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
if (post.user_id) {
|
if (post.user_id && post.user_id !== currentUserId) {
|
||||||
userIds.add(post.user_id);
|
userIdsToFetchStatusFor.add(post.user_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We also fetch for users who have DMs open with the current user
|
||||||
const directShowPreferences = getDirectShowPreferences(state);
|
const directShowPreferences = getDirectShowPreferences(state);
|
||||||
for (const directShowPreference of directShowPreferences) {
|
for (const directShowPreference of directShowPreferences) {
|
||||||
if (directShowPreference.value === 'true') {
|
if (directShowPreference.value === 'true') {
|
||||||
// This is the other user's id in the DM
|
// This is the other user's id in the DM
|
||||||
userIds.add(directShowPreference.name);
|
userIdsToFetchStatusFor.add(directShowPreference.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserId = getCurrentUserId(state);
|
// Both the users in the DM list and recent posts constitute for all the visible users in the current channel
|
||||||
userIds.add(currentUserId);
|
const userIdsForStatus = Array.from(userIdsToFetchStatusFor);
|
||||||
|
if (userIdsForStatus.length > 0) {
|
||||||
dispatch(loadStatusesByIds(Array.from(userIds)));
|
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForStatus}));
|
||||||
|
}
|
||||||
|
|
||||||
return {data: true};
|
return {data: true};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ import {
|
|||||||
getCustomEmojiForReaction,
|
getCustomEmojiForReaction,
|
||||||
getPosts,
|
getPosts,
|
||||||
getPostThread,
|
getPostThread,
|
||||||
getMentionsAndStatusesForPosts,
|
|
||||||
getPostThreads,
|
getPostThreads,
|
||||||
postDeleted,
|
postDeleted,
|
||||||
receivedNewPost,
|
receivedNewPost,
|
||||||
receivedPost,
|
receivedPost,
|
||||||
} from 'mattermost-redux/actions/posts';
|
} from 'mattermost-redux/actions/posts';
|
||||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||||
|
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import * as TeamActions from 'mattermost-redux/actions/teams';
|
import * as TeamActions from 'mattermost-redux/actions/teams';
|
||||||
import {
|
import {
|
||||||
getThread as fetchThread,
|
getThread as fetchThread,
|
||||||
@@ -74,6 +74,7 @@ import {
|
|||||||
getCurrentChannelId,
|
getCurrentChannelId,
|
||||||
getRedirectChannelNameForTeam,
|
getRedirectChannelNameForTeam,
|
||||||
} from 'mattermost-redux/selectors/entities/channels';
|
} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getGroup} from 'mattermost-redux/selectors/entities/groups';
|
import {getGroup} from 'mattermost-redux/selectors/entities/groups';
|
||||||
import {getPost, getMostRecentPostIdInChannel, getTeamIdFromPost} from 'mattermost-redux/selectors/entities/posts';
|
import {getPost, getMostRecentPostIdInChannel, getTeamIdFromPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
@@ -241,7 +242,11 @@ export function reconnect() {
|
|||||||
// we can request for getPosts again when socket is connected
|
// we can request for getPosts again when socket is connected
|
||||||
dispatch(getPosts(currentChannelId));
|
dispatch(getPosts(currentChannelId));
|
||||||
}
|
}
|
||||||
dispatch(StatusActions.loadStatusesForChannelAndSidebar());
|
|
||||||
|
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
||||||
|
if (enabledUserStatuses) {
|
||||||
|
dispatch(StatusActions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||||
|
}
|
||||||
|
|
||||||
const crtEnabled = isCollapsedThreadsEnabled(state);
|
const crtEnabled = isCollapsedThreadsEnabled(state);
|
||||||
dispatch(TeamActions.getMyTeamUnreads(crtEnabled, true));
|
dispatch(TeamActions.getMyTeamUnreads(crtEnabled, true));
|
||||||
@@ -702,8 +707,7 @@ export function handleNewPostEvent(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
myDispatch(handleNewPost(post, msg));
|
myDispatch(handleNewPost(post, msg));
|
||||||
|
myDispatch(batchFetchStatusesProfilesGroupsFromPosts([post]));
|
||||||
getMentionsAndStatusesForPosts([post], myDispatch, myGetState);
|
|
||||||
|
|
||||||
// Since status updates aren't real time, assume another user is online if they have posted and:
|
// Since status updates aren't real time, assume another user is online if they have posted and:
|
||||||
// 1. The user hasn't set their status manually to something that isn't online
|
// 1. The user hasn't set their status manually to something that isn't online
|
||||||
@@ -746,9 +750,7 @@ export function handleNewPostEvents(queue) {
|
|||||||
|
|
||||||
// Load the posts' threads
|
// Load the posts' threads
|
||||||
myDispatch(getPostThreads(posts));
|
myDispatch(getPostThreads(posts));
|
||||||
|
myDispatch(batchFetchStatusesProfilesGroupsFromPosts(posts));
|
||||||
// And any other data needed for them
|
|
||||||
getMentionsAndStatusesForPosts(posts, myDispatch, myGetState);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,7 +766,7 @@ export function handlePostEditEvent(msg) {
|
|||||||
const crtEnabled = isCollapsedThreadsEnabled(getState());
|
const crtEnabled = isCollapsedThreadsEnabled(getState());
|
||||||
dispatch(receivedPost(post, crtEnabled));
|
dispatch(receivedPost(post, crtEnabled));
|
||||||
|
|
||||||
getMentionsAndStatusesForPosts([post], dispatch, getState);
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts([post]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePostDeleteEvent(msg) {
|
async function handlePostDeleteEvent(msg) {
|
||||||
@@ -1141,7 +1143,7 @@ export async function handleUserUpdatedEvent(msg) {
|
|||||||
|
|
||||||
if (currentUser.id === user.id) {
|
if (currentUser.id === user.id) {
|
||||||
if (user.update_at > currentUser.update_at) {
|
if (user.update_at > currentUser.update_at) {
|
||||||
// update user to unsanitized user data recieved from websocket message
|
// update user to unsanitized user data received from websocket message
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UserTypes.RECEIVED_ME,
|
type: UserTypes.RECEIVED_ME,
|
||||||
data: user,
|
data: user,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
import {UserTypes, CloudTypes} from 'mattermost-redux/action_types';
|
import {UserTypes, CloudTypes} from 'mattermost-redux/action_types';
|
||||||
import {getGroup} from 'mattermost-redux/actions/groups';
|
import {getGroup} from 'mattermost-redux/actions/groups';
|
||||||
import {
|
import {
|
||||||
getMentionsAndStatusesForPosts,
|
|
||||||
getPostThreads,
|
getPostThreads,
|
||||||
receivedNewPost,
|
receivedNewPost,
|
||||||
} from 'mattermost-redux/actions/posts';
|
} from 'mattermost-redux/actions/posts';
|
||||||
|
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import {getUser} from 'mattermost-redux/actions/users';
|
import {getUser} from 'mattermost-redux/actions/users';
|
||||||
|
|
||||||
import {handleNewPost} from 'actions/post_actions';
|
import {handleNewPost} from 'actions/post_actions';
|
||||||
@@ -44,6 +44,11 @@ jest.mock('mattermost-redux/actions/posts', () => ({
|
|||||||
getMentionsAndStatusesForPosts: jest.fn(),
|
getMentionsAndStatusesForPosts: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('mattermost-redux/actions/status_profile_polling', () => ({
|
||||||
|
...jest.requireActual('mattermost-redux/actions/status_profile_polling'),
|
||||||
|
batchFetchStatusesProfilesGroupsFromPosts: jest.fn(() => ({type: ''})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('mattermost-redux/actions/groups', () => ({
|
jest.mock('mattermost-redux/actions/groups', () => ({
|
||||||
...jest.requireActual('mattermost-redux/actions/groups'),
|
...jest.requireActual('mattermost-redux/actions/groups'),
|
||||||
getGroup: jest.fn(() => ({type: 'RECEIVED_GROUP'})),
|
getGroup: jest.fn(() => ({type: 'RECEIVED_GROUP'})),
|
||||||
@@ -488,8 +493,8 @@ describe('handleNewPostEvent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
testStore.dispatch(handleNewPostEvent(msg));
|
testStore.dispatch(handleNewPostEvent(msg));
|
||||||
expect(getMentionsAndStatusesForPosts).toHaveBeenCalledWith([post], expect.anything(), expect.anything());
|
|
||||||
expect(handleNewPost).toHaveBeenCalledWith(post, msg);
|
expect(handleNewPost).toHaveBeenCalledWith(post, msg);
|
||||||
|
expect(batchFetchStatusesProfilesGroupsFromPosts).toHaveBeenCalledWith([post]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should set other user to online', () => {
|
test('should set other user to online', () => {
|
||||||
@@ -609,18 +614,17 @@ describe('handleNewPostEvents', () => {
|
|||||||
|
|
||||||
testStore.dispatch(handleNewPostEvents(queue));
|
testStore.dispatch(handleNewPostEvents(queue));
|
||||||
|
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()[0]).toEqual({
|
||||||
{
|
type: 'BATCHING_REDUCER.BATCH',
|
||||||
meta: {batch: true},
|
meta: {batch: true},
|
||||||
payload: posts.map((post) => receivedNewPost(post, false)),
|
payload: posts.map((post) => receivedNewPost(post, false)),
|
||||||
type: 'BATCHING_REDUCER.BATCH',
|
});
|
||||||
},
|
expect(testStore.getActions()[1]).toEqual({
|
||||||
{
|
|
||||||
type: 'GET_THREADS_FOR_POSTS',
|
type: 'GET_THREADS_FOR_POSTS',
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
expect(getPostThreads).toHaveBeenCalledWith(posts);
|
expect(getPostThreads).toHaveBeenCalledWith(posts);
|
||||||
expect(getMentionsAndStatusesForPosts).toHaveBeenCalledWith(posts, expect.anything(), expect.anything());
|
expect(batchFetchStatusesProfilesGroupsFromPosts).toHaveBeenCalledWith(posts);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6146,6 +6146,15 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
it.stateIsFalse('ServiceSettings.EnableUserTypingMessages'),
|
it.stateIsFalse('ServiceSettings.EnableUserTypingMessages'),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
key: 'ExperimentalSettings.UsersStatusAndProfileFetchingPollIntervalMilliseconds',
|
||||||
|
label: defineMessage({id: 'admin.experimental.UsersStatusAndProfileFetchingPollIntervalMilliseconds.title', defaultMessage: 'User\'s Status and Profile Fetching Poll Interval:'}),
|
||||||
|
help_text: defineMessage({id: 'admin.experimental.UsersStatusAndProfileFetchingPollIntervalMilliseconds.desc', defaultMessage: 'The number of milliseconds to wait between fetching user statuses and profiles periodically.'}),
|
||||||
|
help_text_markdown: false,
|
||||||
|
placeholder: defineMessage({id: 'admin.experimental.timeBetweenUserTypingUpdatesMilliseconds.example', defaultMessage: 'E.g.: "5000"'}),
|
||||||
|
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
key: 'TeamSettings.ExperimentalPrimaryTeam',
|
key: 'TeamSettings.ExperimentalPrimaryTeam',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jest.mock('components/product_notices_modal', () => () => <div/>);
|
|||||||
jest.mock('plugins/pluggable', () => () => <div/>);
|
jest.mock('plugins/pluggable', () => () => <div/>);
|
||||||
|
|
||||||
jest.mock('actions/status_actions', () => ({
|
jest.mock('actions/status_actions', () => ({
|
||||||
loadStatusesForChannelAndSidebar: jest.fn().mockImplementation(() => () => {}),
|
addVisibleUsersInCurrentChannelToStatusPoll: jest.fn().mockImplementation(() => () => {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('mattermost-redux/selectors/entities/general', () => ({
|
jest.mock('mattermost-redux/selectors/entities/general', () => ({
|
||||||
@@ -46,7 +46,7 @@ describe('ChannelController', () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches loadStatusesForChannelAndSidebar when enableUserStatuses is true', () => {
|
it('dispatches addVisibleUsersInCurrentChannelToStatusPoll when enableUserStatuses is true', () => {
|
||||||
mockState.entities.general.config.EnableUserStatuses = 'true';
|
mockState.entities.general.config.EnableUserStatuses = 'true';
|
||||||
const store = mockStore(mockState);
|
const store = mockStore(mockState);
|
||||||
|
|
||||||
@@ -60,10 +60,10 @@ describe('ChannelController', () => {
|
|||||||
jest.advanceTimersByTime(Constants.STATUS_INTERVAL);
|
jest.advanceTimersByTime(Constants.STATUS_INTERVAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(actions.loadStatusesForChannelAndSidebar).toHaveBeenCalled();
|
expect(actions.addVisibleUsersInCurrentChannelToStatusPoll).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not dispatch loadStatusesForChannelAndSidebar when enableUserStatuses is false', () => {
|
it('does not dispatch addVisibleUsersInCurrentChannelToStatusPoll when enableUserStatuses is false', () => {
|
||||||
const store = mockStore(mockState);
|
const store = mockStore(mockState);
|
||||||
mockState.entities.general.config.EnableUserStatuses = 'false';
|
mockState.entities.general.config.EnableUserStatuses = 'false';
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ describe('ChannelController', () => {
|
|||||||
jest.advanceTimersByTime(Constants.STATUS_INTERVAL);
|
jest.advanceTimersByTime(Constants.STATUS_INTERVAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(actions.loadStatusesForChannelAndSidebar).not.toHaveBeenCalled();
|
expect(actions.addVisibleUsersInCurrentChannelToStatusPoll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import classNames from 'classnames';
|
|||||||
import React, {useEffect} from 'react';
|
import React, {useEffect} from 'react';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {cleanUpStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
||||||
|
|
||||||
import {loadStatusesForChannelAndSidebar} from 'actions/status_actions';
|
import {addVisibleUsersInCurrentChannelToStatusPoll} from 'actions/status_actions';
|
||||||
|
|
||||||
import CenterChannel from 'components/channel_layout/center_channel';
|
import CenterChannel from 'components/channel_layout/center_channel';
|
||||||
import LoadingScreen from 'components/loading_screen';
|
import LoadingScreen from 'components/loading_screen';
|
||||||
@@ -40,6 +41,10 @@ export default function ChannelController(props: Props) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.classList.remove(...BODY_CLASS_FOR_CHANNEL);
|
document.body.classList.remove(...BODY_CLASS_FOR_CHANNEL);
|
||||||
|
|
||||||
|
// This cleans up the status and profile setInterval of fetching poll we use to batch requests
|
||||||
|
// when fetching statuses and profiles for a list of users.
|
||||||
|
cleanUpStatusAndProfileFetchingPoll();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -47,14 +52,14 @@ export default function ChannelController(props: Props) {
|
|||||||
let loadStatusesIntervalId: NodeJS.Timeout;
|
let loadStatusesIntervalId: NodeJS.Timeout;
|
||||||
if (enabledUserStatuses) {
|
if (enabledUserStatuses) {
|
||||||
loadStatusesIntervalId = setInterval(() => {
|
loadStatusesIntervalId = setInterval(() => {
|
||||||
dispatch(loadStatusesForChannelAndSidebar());
|
dispatch(addVisibleUsersInCurrentChannelToStatusPoll());
|
||||||
}, Constants.STATUS_INTERVAL);
|
}, Constants.STATUS_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(loadStatusesIntervalId);
|
clearInterval(loadStatusesIntervalId);
|
||||||
};
|
};
|
||||||
}, [dispatch, enabledUserStatuses]);
|
}, [enabledUserStatuses]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/prefere
|
|||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {loadStatusesForChannelAndSidebar} from 'actions/status_actions';
|
import {addVisibleUsersInCurrentChannelToStatusPoll} from 'actions/status_actions';
|
||||||
import {addUserToTeam} from 'actions/team_actions';
|
import {addUserToTeam} from 'actions/team_actions';
|
||||||
import LocalStorageStore from 'stores/local_storage_store';
|
import LocalStorageStore from 'stores/local_storage_store';
|
||||||
|
|
||||||
@@ -40,11 +40,8 @@ export function initializeTeam(team: Team): ActionFuncAsync<Team, GlobalState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
||||||
|
|
||||||
if (enabledUserStatuses) {
|
if (enabledUserStatuses) {
|
||||||
// This is the first time in the pool of user statuses that we request,
|
dispatch(addVisibleUsersInCurrentChannelToStatusPoll());
|
||||||
// subsequent requests will be done via setInterval at channel_controller.tsx
|
|
||||||
dispatch(loadStatusesForChannelAndSidebar());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const license = getLicense(state);
|
const license = getLicense(state);
|
||||||
|
|||||||
@@ -989,6 +989,8 @@
|
|||||||
"admin.experimental.timeBetweenUserTypingUpdatesMilliseconds.title": "User Typing Timeout:",
|
"admin.experimental.timeBetweenUserTypingUpdatesMilliseconds.title": "User Typing Timeout:",
|
||||||
"admin.experimental.useChannelInEmailNotifications.desc": "When true, channel and team name appears in email notification subject lines. Useful for servers using only one team. When false, only team name appears in email notification subject line.",
|
"admin.experimental.useChannelInEmailNotifications.desc": "When true, channel and team name appears in email notification subject lines. Useful for servers using only one team. When false, only team name appears in email notification subject line.",
|
||||||
"admin.experimental.useChannelInEmailNotifications.title": "Use Channel Name in Email Notifications:",
|
"admin.experimental.useChannelInEmailNotifications.title": "Use Channel Name in Email Notifications:",
|
||||||
|
"admin.experimental.UsersStatusAndProfileFetchingPollIntervalMilliseconds.desc": "The number of milliseconds to wait between fetching user statuses and profiles periodically.",
|
||||||
|
"admin.experimental.UsersStatusAndProfileFetchingPollIntervalMilliseconds.title": "User's Status and Profile Fetching Poll Interval:",
|
||||||
"admin.experimental.userStatusAwayTimeout.desc": "This setting defines the number of seconds after which the user’s status indicator changes to \"Away\", when they are away from Mattermost.",
|
"admin.experimental.userStatusAwayTimeout.desc": "This setting defines the number of seconds after which the user’s status indicator changes to \"Away\", when they are away from Mattermost.",
|
||||||
"admin.experimental.userStatusAwayTimeout.example": "E.g.: \"300\"",
|
"admin.experimental.userStatusAwayTimeout.example": "E.g.: \"300\"",
|
||||||
"admin.experimental.userStatusAwayTimeout.title": "User Status Away Timeout:",
|
"admin.experimental.userStatusAwayTimeout.title": "User Status Away Timeout:",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export default keyMirror({
|
|||||||
|
|
||||||
GET_POSTS_SUCCESS: null,
|
GET_POSTS_SUCCESS: null,
|
||||||
GET_POSTS_FAILURE: null,
|
GET_POSTS_FAILURE: null,
|
||||||
GET_POSTS_SINCE_SUCCESS: null,
|
|
||||||
|
|
||||||
GET_POST_THREAD_WITH_RETRY_ATTEMPT: null,
|
GET_POST_THREAD_WITH_RETRY_ATTEMPT: null,
|
||||||
GET_POSTS_WITH_RETRY_ATTEMPT: null,
|
GET_POSTS_WITH_RETRY_ATTEMPT: null,
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ import {Preferences, Posts, RequestStatus} from '../constants';
|
|||||||
|
|
||||||
const OK_RESPONSE = {status: 'OK'};
|
const OK_RESPONSE = {status: 'OK'};
|
||||||
|
|
||||||
|
jest.mock('mattermost-redux/actions/status_profile_polling', () => ({
|
||||||
|
batchFetchStatusesProfilesGroupsFromPosts: jest.fn(() => {
|
||||||
|
return {type: ''};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Actions.Posts', () => {
|
describe('Actions.Posts', () => {
|
||||||
let store = configureStore();
|
let store = configureStore();
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -1633,6 +1639,7 @@ describe('getPostThreads', () => {
|
|||||||
users: {
|
users: {
|
||||||
currentUserId: 'currentUserId',
|
currentUserId: 'currentUserId',
|
||||||
statuses: {},
|
statuses: {},
|
||||||
|
profiles: {},
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
config: {},
|
config: {},
|
||||||
@@ -1671,15 +1678,15 @@ describe('getPostThreads', () => {
|
|||||||
|
|
||||||
await testStore.dispatch(Actions.getPostThreads([comment]));
|
await testStore.dispatch(Actions.getPostThreads([comment]));
|
||||||
|
|
||||||
expect(testStore.getActions()[1].payload[0].type).toEqual('RECEIVED_POSTS');
|
expect(testStore.getActions()[0].payload[0].type).toEqual('RECEIVED_POSTS');
|
||||||
expect(testStore.getActions()[1].payload[0].data.posts).toEqual({
|
expect(testStore.getActions()[0].payload[0].data.posts).toEqual({
|
||||||
[post1.id]: post1,
|
[post1.id]: post1,
|
||||||
[comment.id]: comment,
|
[comment.id]: comment,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testStore.getActions()[1].payload[1].type).toEqual('RECEIVED_POSTS_IN_THREAD');
|
expect(testStore.getActions()[0].payload[1].type).toEqual('RECEIVED_POSTS_IN_THREAD');
|
||||||
expect(testStore.getActions()[1].payload[1].rootId).toEqual(post1.id);
|
expect(testStore.getActions()[0].payload[1].rootId).toEqual(post1.id);
|
||||||
expect(testStore.getActions()[1].payload[1].data.posts).toEqual({
|
expect(testStore.getActions()[0].payload[1].data.posts).toEqual({
|
||||||
[post1.id]: post1,
|
[post1.id]: post1,
|
||||||
[comment.id]: comment,
|
[comment.id]: comment,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
deletePreferences,
|
deletePreferences,
|
||||||
savePreferences,
|
savePreferences,
|
||||||
} from 'mattermost-redux/actions/preferences';
|
} from 'mattermost-redux/actions/preferences';
|
||||||
|
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import {decrementThreadCounts} from 'mattermost-redux/actions/threads';
|
import {decrementThreadCounts} from 'mattermost-redux/actions/threads';
|
||||||
import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'mattermost-redux/actions/users';
|
import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||||
import {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE} from 'mattermost-redux/client';
|
import {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE} from 'mattermost-redux/client';
|
||||||
@@ -153,7 +154,6 @@ export function getPost(postId: string): ActionFuncAsync<Post> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
post = await Client4.getPost(postId);
|
post = await Client4.getPost(postId);
|
||||||
getMentionsAndStatusesForPosts([post], dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch({type: PostTypes.GET_POSTS_FAILURE, error});
|
dispatch({type: PostTypes.GET_POSTS_FAILURE, error});
|
||||||
@@ -161,12 +161,8 @@ export function getPost(postId: string): ActionFuncAsync<Post> {
|
|||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(batchActions([
|
dispatch(receivedPost(post, crtEnabled));
|
||||||
receivedPost(post, crtEnabled),
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts([post]));
|
||||||
{
|
|
||||||
type: PostTypes.GET_POSTS_SUCCESS,
|
|
||||||
},
|
|
||||||
]));
|
|
||||||
|
|
||||||
return {data: post};
|
return {data: post};
|
||||||
};
|
};
|
||||||
@@ -709,16 +705,15 @@ async function getPaginatedPostThread(rootId: string, options: FetchPaginatedThr
|
|||||||
|
|
||||||
export function getPostThread(rootId: string, fetchThreads = true): ActionFuncAsync<PostList> {
|
export function getPostThread(rootId: string, fetchThreads = true): ActionFuncAsync<PostList> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
dispatch({type: PostTypes.GET_POST_THREAD_REQUEST});
|
const state = getState();
|
||||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||||
|
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
||||||
|
|
||||||
let posts;
|
let posts;
|
||||||
try {
|
try {
|
||||||
posts = await getPaginatedPostThread(rootId, {fetchThreads, collapsedThreads: collapsedThreadsEnabled});
|
posts = await getPaginatedPostThread(rootId, {fetchThreads, collapsedThreads: collapsedThreadsEnabled});
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch({type: PostTypes.GET_POST_THREAD_FAILURE, error});
|
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
@@ -726,11 +721,12 @@ export function getPostThread(rootId: string, fetchThreads = true): ActionFuncAs
|
|||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsInThread(posts, rootId),
|
receivedPostsInThread(posts, rootId),
|
||||||
{
|
|
||||||
type: PostTypes.GET_POST_THREAD_SUCCESS,
|
|
||||||
},
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
if (enabledUserStatuses) {
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
}
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -756,7 +752,6 @@ export function getNewestPostThread(rootId: string): ActionFuncAsync {
|
|||||||
let posts;
|
let posts;
|
||||||
try {
|
try {
|
||||||
posts = await getPaginatedPostThread(rootId, options);
|
posts = await getPaginatedPostThread(rootId, options);
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch({type: PostTypes.GET_POST_THREAD_FAILURE, error});
|
dispatch({type: PostTypes.GET_POST_THREAD_FAILURE, error});
|
||||||
@@ -767,10 +762,8 @@ export function getNewestPostThread(rootId: string): ActionFuncAsync {
|
|||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsInThread(posts, rootId),
|
receivedPostsInThread(posts, rootId),
|
||||||
{
|
|
||||||
type: PostTypes.GET_POST_THREAD_SUCCESS,
|
|
||||||
},
|
|
||||||
]));
|
]));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
@@ -782,7 +775,6 @@ export function getPosts(channelId: string, page = 0, perPage = Posts.POST_CHUNK
|
|||||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||||
try {
|
try {
|
||||||
posts = await Client4.getPosts(channelId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
posts = await Client4.getPosts(channelId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -793,6 +785,7 @@ export function getPosts(channelId: string, page = 0, perPage = Posts.POST_CHUNK
|
|||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsInChannel(posts, channelId, page === 0, posts.prev_post_id === ''),
|
receivedPostsInChannel(posts, channelId, page === 0, posts.prev_post_id === ''),
|
||||||
]));
|
]));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
@@ -800,9 +793,11 @@ export function getPosts(channelId: string, page = 0, perPage = Posts.POST_CHUNK
|
|||||||
|
|
||||||
export function getPostsUnread(channelId: string, fetchThreads = true, collapsedThreadsExtended = false): ActionFuncAsync<PostList> {
|
export function getPostsUnread(channelId: string, fetchThreads = true, collapsedThreadsExtended = false): ActionFuncAsync<PostList> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const shouldLoadRecent = getUnreadScrollPositionPreference(getState()) === Preferences.UNREAD_SCROLL_POSITION_START_FROM_NEWEST;
|
const state = getState();
|
||||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
const shouldLoadRecent = getUnreadScrollPositionPreference(state) === Preferences.UNREAD_SCROLL_POSITION_START_FROM_NEWEST;
|
||||||
const userId = getCurrentUserId(getState());
|
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||||
|
const userId = getCurrentUserId(state);
|
||||||
|
|
||||||
let posts;
|
let posts;
|
||||||
let recentPosts;
|
let recentPosts;
|
||||||
try {
|
try {
|
||||||
@@ -811,29 +806,29 @@ export function getPostsUnread(channelId: string, fetchThreads = true, collapsed
|
|||||||
if (posts.next_post_id && shouldLoadRecent) {
|
if (posts.next_post_id && shouldLoadRecent) {
|
||||||
recentPosts = await Client4.getPosts(channelId, 0, Posts.POST_CHUNK_SIZE / 2, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
recentPosts = await Client4.getPosts(channelId, 0, Posts.POST_CHUNK_SIZE / 2, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
|
|
||||||
const recentPostsActions = recentPosts ? [
|
const actions: AnyAction[] = [
|
||||||
receivedPosts(recentPosts),
|
{
|
||||||
receivedPostsInChannel(recentPosts, channelId, recentPosts.next_post_id === '', recentPosts.prev_post_id === ''),
|
|
||||||
] : [];
|
|
||||||
|
|
||||||
dispatch(batchActions([
|
|
||||||
receivedPosts(posts),
|
|
||||||
receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''),
|
|
||||||
...recentPostsActions,
|
|
||||||
]));
|
|
||||||
dispatch({
|
|
||||||
type: PostTypes.RECEIVED_POSTS,
|
type: PostTypes.RECEIVED_POSTS,
|
||||||
data: posts,
|
data: posts,
|
||||||
channelId,
|
channelId,
|
||||||
});
|
},
|
||||||
|
receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''),
|
||||||
|
];
|
||||||
|
if (recentPosts) {
|
||||||
|
actions.push(
|
||||||
|
receivedPosts(recentPosts),
|
||||||
|
receivedPostsInChannel(recentPosts, channelId, recentPosts.next_post_id === '', recentPosts.prev_post_id === ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(batchActions(actions));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
@@ -845,7 +840,6 @@ export function getPostsSince(channelId: string, since: number, fetchThreads = t
|
|||||||
try {
|
try {
|
||||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||||
posts = await Client4.getPostsSince(channelId, since, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
posts = await Client4.getPostsSince(channelId, since, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -855,10 +849,8 @@ export function getPostsSince(channelId: string, since: number, fetchThreads = t
|
|||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsSince(posts, channelId),
|
receivedPostsSince(posts, channelId),
|
||||||
{
|
|
||||||
type: PostTypes.GET_POSTS_SINCE_SUCCESS,
|
|
||||||
},
|
|
||||||
]));
|
]));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
@@ -870,7 +862,6 @@ export function getPostsBefore(channelId: string, postId: string, page = 0, perP
|
|||||||
try {
|
try {
|
||||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||||
posts = await Client4.getPostsBefore(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
posts = await Client4.getPostsBefore(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -881,6 +872,7 @@ export function getPostsBefore(channelId: string, postId: string, page = 0, perP
|
|||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsBefore(posts, channelId, postId, posts.prev_post_id === ''),
|
receivedPostsBefore(posts, channelId, postId, posts.prev_post_id === ''),
|
||||||
]));
|
]));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
@@ -892,7 +884,6 @@ export function getPostsAfter(channelId: string, postId: string, page = 0, perPa
|
|||||||
try {
|
try {
|
||||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||||
posts = await Client4.getPostsAfter(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
posts = await Client4.getPostsAfter(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -903,6 +894,7 @@ export function getPostsAfter(channelId: string, postId: string, page = 0, perPa
|
|||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsAfter(posts, channelId, postId, posts.next_post_id === ''),
|
receivedPostsAfter(posts, channelId, postId, posts.next_post_id === ''),
|
||||||
]));
|
]));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
@@ -944,12 +936,11 @@ export function getPostsAround(channelId: string, postId: string, perPage = Post
|
|||||||
first_inaccessible_post_time: Math.max(before.first_inaccessible_post_time, after.first_inaccessible_post_time, thread.first_inaccessible_post_time) || 0,
|
first_inaccessible_post_time: Math.max(before.first_inaccessible_post_time, after.first_inaccessible_post_time, thread.first_inaccessible_post_time) || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
|
||||||
|
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
receivedPosts(posts),
|
receivedPosts(posts),
|
||||||
receivedPostsInChannel(posts, channelId, after.next_post_id === '', before.prev_post_id === ''),
|
receivedPostsInChannel(posts, channelId, after.next_post_id === '', before.prev_post_id === ''),
|
||||||
]));
|
]));
|
||||||
|
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||||
|
|
||||||
return {data: posts};
|
return {data: posts};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {GroupSearchParams} from '@mattermost/types/groups';
|
||||||
|
import type {PostList, Post, PostAcknowledgement, PostEmbed, PostPreviewMetadata} from '@mattermost/types/posts';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {searchGroups} from 'mattermost-redux/actions/groups';
|
||||||
|
import {getNeededAtMentionedUsernamesAndGroups} from 'mattermost-redux/actions/posts';
|
||||||
|
import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||||
|
import {getCurrentUser, getCurrentUserId, getIsUserStatusesConfigEnabled, getUsers} from 'mattermost-redux/selectors/entities/common';
|
||||||
|
import {getUsersStatusAndProfileFetchingPollInterval} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getUserStatuses} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import type {ActionFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
|
const MAX_USER_IDS_PER_STATUS_REQUEST = 200; // users ids per 'users/status/ids'request
|
||||||
|
const MAX_USER_IDS_PER_PROFILES_REQUEST = 100; // users ids per 'users/ids' request
|
||||||
|
|
||||||
|
const pendingUserIdsForStatuses = new Set<string>();
|
||||||
|
const pendingUserIdsForProfiles = new Set<string>();
|
||||||
|
|
||||||
|
let intervalIdForFetchingPoll: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
type UserIdsSingleOrArray = Array<UserProfile['id']> | UserProfile['id'];
|
||||||
|
type AddUserIdsForStatusAndProfileFetchingPoll = {
|
||||||
|
userIdsForStatus?: UserIdsSingleOrArray;
|
||||||
|
userIdsForProfile?: UserIdsSingleOrArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds list(s) of user id(s) to the status and profile fetching poll. Which gets fetched based on user interval polling duration
|
||||||
|
* Do not use if status or profile is required immediately.
|
||||||
|
*/
|
||||||
|
export function addUserIdsForStatusAndProfileFetchingPoll({userIdsForStatus, userIdsForProfile}: AddUserIdsForStatusAndProfileFetchingPoll): ActionFunc<boolean> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
function getPendingStatusesById() {
|
||||||
|
// Since we can only fetch a defined number of user statuses at a time, we need to batch the requests
|
||||||
|
if (pendingUserIdsForStatuses.size >= MAX_USER_IDS_PER_STATUS_REQUEST) {
|
||||||
|
// We use temp buffer here to store up until max buffer size
|
||||||
|
// and clear out processed user ids
|
||||||
|
const bufferedUserIds: string[] = [];
|
||||||
|
let bufferCounter = 0;
|
||||||
|
for (const pendingUserId of pendingUserIdsForStatuses) {
|
||||||
|
if (pendingUserId.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedUserIds.push(pendingUserId);
|
||||||
|
pendingUserIdsForStatuses.delete(pendingUserId);
|
||||||
|
|
||||||
|
bufferCounter++;
|
||||||
|
|
||||||
|
if (bufferCounter >= MAX_USER_IDS_PER_STATUS_REQUEST) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferedUserIds.length > 0) {
|
||||||
|
dispatch(getStatusesByIds(bufferedUserIds));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we have less than max buffer size, we can directly fetch the statuses
|
||||||
|
const lessThanBufferUserIds = Array.from(pendingUserIdsForStatuses);
|
||||||
|
if (lessThanBufferUserIds.length > 0) {
|
||||||
|
dispatch(getStatusesByIds(lessThanBufferUserIds));
|
||||||
|
|
||||||
|
pendingUserIdsForStatuses.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPendingProfilesById() {
|
||||||
|
if (pendingUserIdsForProfiles.size >= MAX_USER_IDS_PER_PROFILES_REQUEST) {
|
||||||
|
const bufferedUserIds: Array<UserProfile['id']> = [];
|
||||||
|
let bufferCounter = 0;
|
||||||
|
for (const pendingUserId of pendingUserIdsForProfiles) {
|
||||||
|
if (pendingUserId.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedUserIds.push(pendingUserId);
|
||||||
|
pendingUserIdsForProfiles.delete(pendingUserId);
|
||||||
|
|
||||||
|
bufferCounter++;
|
||||||
|
|
||||||
|
// We can only fetch a defined number of user profiles at a time
|
||||||
|
// So we break out of the loop if we reach the max batch size
|
||||||
|
if (bufferCounter >= MAX_USER_IDS_PER_PROFILES_REQUEST) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferedUserIds.length > 0) {
|
||||||
|
dispatch(getProfilesByIds(bufferedUserIds));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const lessThanBufferUserIds = Array.from(pendingUserIdsForProfiles);
|
||||||
|
if (lessThanBufferUserIds.length > 0) {
|
||||||
|
dispatch(getProfilesByIds(lessThanBufferUserIds));
|
||||||
|
|
||||||
|
pendingUserIdsForProfiles.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollingInterval = getUsersStatusAndProfileFetchingPollInterval(getState());
|
||||||
|
|
||||||
|
if (userIdsForStatus) {
|
||||||
|
if (Array.isArray(userIdsForStatus)) {
|
||||||
|
userIdsForStatus.forEach((userId) => {
|
||||||
|
if (userId.length > 0) {
|
||||||
|
pendingUserIdsForStatuses.add(userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pendingUserIdsForStatuses.add(userIdsForStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIdsForProfile) {
|
||||||
|
if (Array.isArray(userIdsForProfile)) {
|
||||||
|
userIdsForProfile.forEach((userId) => {
|
||||||
|
if (userId.length > 0) {
|
||||||
|
pendingUserIdsForProfiles.add(userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pendingUserIdsForProfiles.add(userIdsForProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape hatch to fetch immediately or when we haven't received the polling interval from config yet
|
||||||
|
if (!pollingInterval || pollingInterval <= 0) {
|
||||||
|
if (pendingUserIdsForStatuses.size > 0) {
|
||||||
|
getPendingStatusesById();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingUserIdsForProfiles.size > 0) {
|
||||||
|
getPendingProfilesById();
|
||||||
|
}
|
||||||
|
} else if (intervalIdForFetchingPoll === null) {
|
||||||
|
// Start the interval if it is not already running
|
||||||
|
intervalIdForFetchingPoll = setInterval(() => {
|
||||||
|
if (pendingUserIdsForStatuses.size > 0) {
|
||||||
|
getPendingStatusesById();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingUserIdsForProfiles.size > 0) {
|
||||||
|
getPendingProfilesById();
|
||||||
|
}
|
||||||
|
}, pollingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now here the interval is already running and we have added the user ids to the poll so we don't need to do anything
|
||||||
|
return {data: true};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanUpStatusAndProfileFetchingPoll() {
|
||||||
|
if (intervalIdForFetchingPoll !== null) {
|
||||||
|
clearInterval(intervalIdForFetchingPoll);
|
||||||
|
intervalIdForFetchingPoll = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets in batch the user profiles, user statuses and user groups for the users in the posts list
|
||||||
|
* This action however doesn't refetch the profiles and statuses except for groups if they are already fetched once
|
||||||
|
*/
|
||||||
|
export function batchFetchStatusesProfilesGroupsFromPosts(postsArrayOrMap: Post[]|PostList['posts']|Post): ActionFunc<boolean> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!postsArrayOrMap) {
|
||||||
|
return {data: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
let posts: Post[] = [];
|
||||||
|
if (Array.isArray(postsArrayOrMap)) {
|
||||||
|
posts = postsArrayOrMap;
|
||||||
|
} else if (typeof postsArrayOrMap === 'object' && 'id' in postsArrayOrMap) {
|
||||||
|
posts = [postsArrayOrMap as Post];
|
||||||
|
} else if (typeof postsArrayOrMap === 'object') {
|
||||||
|
posts = Object.values(postsArrayOrMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posts.length === 0) {
|
||||||
|
return {data: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mentionedUsernamesAndGroupsInPosts = new Set<string>();
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const currentUser = getCurrentUser(state);
|
||||||
|
const currentUserId = getCurrentUserId(state);
|
||||||
|
const isUserStatusesConfigEnabled = getIsUserStatusesConfigEnabled(state);
|
||||||
|
const users = getUsers(state);
|
||||||
|
const userStatuses = getUserStatuses(state);
|
||||||
|
|
||||||
|
posts.forEach((post) => {
|
||||||
|
if (post.metadata) {
|
||||||
|
// Add users listed in permalink previews
|
||||||
|
if (post.metadata.embeds) {
|
||||||
|
post.metadata.embeds.forEach((embed: PostEmbed) => {
|
||||||
|
if (embed.type === 'permalink' && embed.data) {
|
||||||
|
const permalinkPostPreviewMetaData = embed.data as PostPreviewMetadata;
|
||||||
|
|
||||||
|
if (permalinkPostPreviewMetaData.post?.user_id && !users[permalinkPostPreviewMetaData.post.user_id] && permalinkPostPreviewMetaData.post.user_id !== currentUserId) {
|
||||||
|
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForProfile: permalinkPostPreviewMetaData.post.user_id}));
|
||||||
|
}
|
||||||
|
if (permalinkPostPreviewMetaData.post?.user_id && !userStatuses[permalinkPostPreviewMetaData.post.user_id] && permalinkPostPreviewMetaData.post.user_id !== currentUserId && isUserStatusesConfigEnabled) {
|
||||||
|
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForStatus: permalinkPostPreviewMetaData.post.user_id}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add users listed in the Post Acknowledgement feature
|
||||||
|
if (post.metadata.acknowledgements) {
|
||||||
|
post.metadata.acknowledgements.forEach((ack: PostAcknowledgement) => {
|
||||||
|
if (ack.acknowledged_at > 0 && ack.user_id && !users[ack.user_id] && ack.user_id !== currentUserId) {
|
||||||
|
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForProfile: ack.user_id}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is sufficient to check if the profile is already fetched
|
||||||
|
// as we receive the websocket events for the profiles changes
|
||||||
|
if (!users[post.user_id] && post.user_id !== currentUserId) {
|
||||||
|
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForProfile: post.user_id}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is sufficient to check if the status is already fetched
|
||||||
|
// as we do the polling for statuses for current channel's channel members every 1 minute in channel_controller
|
||||||
|
if (!userStatuses[post.user_id] && post.user_id !== currentUserId && isUserStatusesConfigEnabled) {
|
||||||
|
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForStatus: post.user_id}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check for all @mentions in the post, they can be either users or groups
|
||||||
|
const mentioned = getNeededAtMentionedUsernamesAndGroups(state, [post]);
|
||||||
|
if (mentioned.size > 0) {
|
||||||
|
mentioned.forEach((atMention) => {
|
||||||
|
if (atMention !== currentUser.username) {
|
||||||
|
mentionedUsernamesAndGroupsInPosts.add(atMention);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mentionedUsernamesAndGroupsInPosts.size > 0) {
|
||||||
|
dispatch(getUsersFromMentionedUsernamesAndGroups(Array.from(mentionedUsernamesAndGroupsInPosts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {data: true};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsersFromMentionedUsernamesAndGroups(usernamesAndGroups: string[]): ActionFuncAsync<string[]> {
|
||||||
|
return async (dispatch) => {
|
||||||
|
// We run the at-mentioned be it user or group through the user profile search
|
||||||
|
const {data: userProfiles} = await dispatch(getProfilesByUsernames(usernamesAndGroups));
|
||||||
|
|
||||||
|
const mentionedUsernames: Array<UserProfile['username']> = [];
|
||||||
|
|
||||||
|
// The user at-mentioned will be the userProfiles
|
||||||
|
if (userProfiles) {
|
||||||
|
for (const user of userProfiles) {
|
||||||
|
if (user && user.username) {
|
||||||
|
mentionedUsernames.push(user.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing usernames from the list will leave only the group names
|
||||||
|
const mentionedGroups = usernamesAndGroups.filter((name) => !mentionedUsernames.includes(name));
|
||||||
|
|
||||||
|
for (const group of mentionedGroups) {
|
||||||
|
const groupSearchParam: GroupSearchParams = {
|
||||||
|
q: group,
|
||||||
|
filter_allow_reference: true,
|
||||||
|
page: 0,
|
||||||
|
per_page: 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(searchGroups(groupSearchParam));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {data: mentionedGroups};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -595,9 +595,9 @@ export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncA
|
|||||||
return {data: []};
|
return {data: []};
|
||||||
}
|
}
|
||||||
|
|
||||||
let recievedStatuses: UserStatus[];
|
let receivedStatuses: UserStatus[];
|
||||||
try {
|
try {
|
||||||
recievedStatuses = await Client4.getStatusesByIds(userIds);
|
receivedStatuses = await Client4.getStatusesByIds(userIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -609,11 +609,11 @@ export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncA
|
|||||||
const isManualStatuses: Record<UserProfile['id'], UserStatus['manual']> = {};
|
const isManualStatuses: Record<UserProfile['id'], UserStatus['manual']> = {};
|
||||||
const lastActivity: Record<UserProfile['id'], UserStatus['last_activity_at']> = {};
|
const lastActivity: Record<UserProfile['id'], UserStatus['last_activity_at']> = {};
|
||||||
|
|
||||||
for (const recievedStatus of recievedStatuses) {
|
for (const receivedStatus of receivedStatuses) {
|
||||||
statuses[recievedStatus.user_id] = recievedStatus?.status ?? '';
|
statuses[receivedStatus.user_id] = receivedStatus?.status ?? '';
|
||||||
dndEndTimes[recievedStatus.user_id] = recievedStatus?.dnd_end_time ?? 0;
|
dndEndTimes[receivedStatus.user_id] = receivedStatus?.dnd_end_time ?? 0;
|
||||||
isManualStatuses[recievedStatus.user_id] = recievedStatus?.manual ?? false;
|
isManualStatuses[receivedStatus.user_id] = receivedStatus?.manual ?? false;
|
||||||
lastActivity[recievedStatus.user_id] = recievedStatus?.last_activity_at ?? 0;
|
lastActivity[receivedStatus.user_id] = receivedStatus?.last_activity_at ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
@@ -637,7 +637,7 @@ export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncA
|
|||||||
'BATCHING_STATUSES',
|
'BATCHING_STATUSES',
|
||||||
));
|
));
|
||||||
|
|
||||||
return {data: recievedStatuses};
|
return {data: receivedStatuses};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,3 +122,16 @@ export const getGiphyFetchInstance: (state: GlobalState) => GiphyFetch | null =
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getUsersStatusAndProfileFetchingPollInterval: (state: GlobalState) => number | null = createSelector(
|
||||||
|
'getUsersStatusAndProfileFetchingPollInterval',
|
||||||
|
getConfig,
|
||||||
|
(config) => {
|
||||||
|
const usersStatusAndProfileFetchingPollInterval = config.UsersStatusAndProfileFetchingPollIntervalMilliseconds;
|
||||||
|
if (usersStatusAndProfileFetchingPollInterval) {
|
||||||
|
return parseInt(usersStatusAndProfileFetchingPollInterval, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ export type ClientConfig = {
|
|||||||
WranglerMoveThreadFromGroupMessageChannelEnable: string;
|
WranglerMoveThreadFromGroupMessageChannelEnable: string;
|
||||||
ServiceEnvironment: string;
|
ServiceEnvironment: string;
|
||||||
UniqueEmojiReactionLimitPerPost: string;
|
UniqueEmojiReactionLimitPerPost: string;
|
||||||
|
UsersStatusAndProfileFetchingPollIntervalMilliseconds: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type License = {
|
export type License = {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user