[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
Этот коммит содержится в:
M-ZubairAhmed
2024-05-27 12:34:38 +00:00
коммит произвёл GitHub
родитель b00b68920d
Коммит 273a999167
18 изменённых файлов: 469 добавлений и 126 удалений

Просмотреть файл

@@ -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"

Просмотреть файл

@@ -178,7 +178,8 @@ const (
NativeappSettingsDefaultAndroidAppDownloadLink = "https://mattermost.com/pl/android-app/" NativeappSettingsDefaultAndroidAppDownloadLink = "https://mattermost.com/pl/android-app/"
NativeappSettingsDefaultIosAppDownloadLink = "https://mattermost.com/pl/ios-app/" NativeappSettingsDefaultIosAppDownloadLink = "https://mattermost.com/pl/ios-app/"
ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000 ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000
ExperimentalSettingsDefaultUsersStatusAndProfileFetchingPollIntervalMilliseconds = 3000
AnalyticsSettingsDefaultMaxUsersForStatistics = 2500 AnalyticsSettingsDefaultMaxUsersForStatistics = 2500
@@ -1010,16 +1011,17 @@ func (s *MetricsSettings) SetDefaults() {
} }
type ExperimentalSettings struct { type ExperimentalSettings struct {
ClientSideCertEnable *bool `access:"experimental_features,cloud_restrictable"` ClientSideCertEnable *bool `access:"experimental_features,cloud_restrictable"`
ClientSideCertCheck *string `access:"experimental_features,cloud_restrictable"` ClientSideCertCheck *string `access:"experimental_features,cloud_restrictable"`
LinkMetadataTimeoutMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"` LinkMetadataTimeoutMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"`
RestrictSystemAdmin *bool `access:"experimental_features,write_restrictable"` RestrictSystemAdmin *bool `access:"experimental_features,write_restrictable"`
EnableSharedChannels *bool `access:"experimental_features"` EnableSharedChannels *bool `access:"experimental_features"`
EnableRemoteClusterService *bool `access:"experimental_features"` EnableRemoteClusterService *bool `access:"experimental_features"`
DisableAppBar *bool `access:"experimental_features"` DisableAppBar *bool `access:"experimental_features"`
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 users 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 users 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 === ''), type: PostTypes.RECEIVED_POSTS,
] : []; data: posts,
channelId,
dispatch(batchActions([ },
receivedPosts(posts),
receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''), receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''),
...recentPostsActions, ];
])); if (recentPosts) {
dispatch({ actions.push(
type: PostTypes.RECEIVED_POSTS, receivedPosts(recentPosts),
data: posts, receivedPostsInChannel(recentPosts, channelId, recentPosts.next_post_id === '', recentPosts.prev_post_id === ''),
channelId, );
}); }
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 = {