[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["DisableWakeUpReconnectHandler"] = strconv.FormatBool(*c.ExperimentalSettings.DisableWakeUpReconnectHandler)
|
||||
props["UsersStatusAndProfileFetchingPollIntervalMilliseconds"] = strconv.FormatInt(*c.ExperimentalSettings.UsersStatusAndProfileFetchingPollIntervalMilliseconds, 10)
|
||||
|
||||
// Set default values for all options that require a license.
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
||||
|
||||
@@ -178,7 +178,8 @@ const (
|
||||
NativeappSettingsDefaultAndroidAppDownloadLink = "https://mattermost.com/pl/android-app/"
|
||||
NativeappSettingsDefaultIosAppDownloadLink = "https://mattermost.com/pl/ios-app/"
|
||||
|
||||
ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000
|
||||
ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000
|
||||
ExperimentalSettingsDefaultUsersStatusAndProfileFetchingPollIntervalMilliseconds = 3000
|
||||
|
||||
AnalyticsSettingsDefaultMaxUsersForStatistics = 2500
|
||||
|
||||
@@ -1010,16 +1011,17 @@ func (s *MetricsSettings) SetDefaults() {
|
||||
}
|
||||
|
||||
type ExperimentalSettings struct {
|
||||
ClientSideCertEnable *bool `access:"experimental_features,cloud_restrictable"`
|
||||
ClientSideCertCheck *string `access:"experimental_features,cloud_restrictable"`
|
||||
LinkMetadataTimeoutMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||
RestrictSystemAdmin *bool `access:"experimental_features,write_restrictable"`
|
||||
EnableSharedChannels *bool `access:"experimental_features"`
|
||||
EnableRemoteClusterService *bool `access:"experimental_features"`
|
||||
DisableAppBar *bool `access:"experimental_features"`
|
||||
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
||||
DelayChannelAutocomplete *bool `access:"experimental_features"`
|
||||
DisableWakeUpReconnectHandler *bool `access:"experimental_features"`
|
||||
ClientSideCertEnable *bool `access:"experimental_features,cloud_restrictable"`
|
||||
ClientSideCertCheck *string `access:"experimental_features,cloud_restrictable"`
|
||||
LinkMetadataTimeoutMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||
RestrictSystemAdmin *bool `access:"experimental_features,write_restrictable"`
|
||||
EnableSharedChannels *bool `access:"experimental_features"`
|
||||
EnableRemoteClusterService *bool `access:"experimental_features"`
|
||||
DisableAppBar *bool `access:"experimental_features"`
|
||||
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
||||
DelayChannelAutocomplete *bool `access:"experimental_features"`
|
||||
DisableWakeUpReconnectHandler *bool `access:"experimental_features"`
|
||||
UsersStatusAndProfileFetchingPollIntervalMilliseconds *int64 `access:"experimental_features"`
|
||||
}
|
||||
|
||||
func (s *ExperimentalSettings) SetDefaults() {
|
||||
@@ -1062,6 +1064,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
||||
if s.DisableWakeUpReconnectHandler == nil {
|
||||
s.DisableWakeUpReconnectHandler = NewBool(false)
|
||||
}
|
||||
|
||||
if s.UsersStatusAndProfileFetchingPollIntervalMilliseconds == nil {
|
||||
s.UsersStatusAndProfileFetchingPollIntervalMilliseconds = NewInt64(ExperimentalSettingsDefaultUsersStatusAndProfileFetchingPollIntervalMilliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
type AnalyticsSettings struct {
|
||||
|
||||
@@ -5,6 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {addUserIdsForStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/status_profile_polling';
|
||||
import {getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||
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> {
|
||||
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', () => {
|
||||
const state = cloneDeep(initialState);
|
||||
const testStore = mockStore(state);
|
||||
testStore.dispatch(Actions.loadStatusesForChannelAndSidebar());
|
||||
expect(getStatusesByIds).toHaveBeenCalledWith((expect as GreatExpectations).arrayContainingExactly(['current_user_id', 'user_id2', 'user_id3']));
|
||||
testStore.dispatch(Actions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||
expect(addUserIdsForStatusAndProfileFetchingPoll).toHaveBeenCalled();
|
||||
expect(addUserIdsForStatusAndProfileFetchingPoll).toHaveBeenCalledWith({userIdsForStatus: ['user_id2', 'user_id3']});
|
||||
});
|
||||
|
||||
test('load statuses with empty channel and user in sidebar', () => {
|
||||
const state = cloneDeep(initialState);
|
||||
state.entities.channels.currentChannelId = 'channel_id2';
|
||||
const testStore = mockStore(state);
|
||||
testStore.dispatch(Actions.loadStatusesForChannelAndSidebar());
|
||||
expect(getStatusesByIds).toHaveBeenCalledWith((expect as GreatExpectations).arrayContainingExactly(['current_user_id', 'user_id3']));
|
||||
testStore.dispatch(Actions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||
expect(addUserIdsForStatusAndProfileFetchingPoll).toHaveBeenCalledWith({userIdsForStatus: ['user_id3']});
|
||||
});
|
||||
|
||||
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.preferences.myPreferences = {};
|
||||
const testStore = mockStore(state);
|
||||
testStore.dispatch(Actions.loadStatusesForChannelAndSidebar());
|
||||
expect(getStatusesByIds).toHaveBeenCalledWith((expect as GreatExpectations).arrayContainingExactly(['current_user_id']));
|
||||
testStore.dispatch(Actions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||
expect(addUserIdsForStatusAndProfileFetchingPoll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {addUserIdsForStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/status_profile_polling';
|
||||
import {getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
||||
@@ -15,36 +16,44 @@ import {loadCustomEmojisForCustomStatusesByUserIds} from 'actions/emoji_actions'
|
||||
|
||||
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) => {
|
||||
const state = getState();
|
||||
|
||||
const channelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const currentChannelId = getCurrentChannelId(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) {
|
||||
const posts = postsInChannel.slice(0, state.views.channel.postVisibility[channelId] || 0);
|
||||
// We fetch for users who have recently posted in the current channel
|
||||
if (postsInChannel && numberOfPostsVisibleInCurrentChannel > 0) {
|
||||
const posts = postsInChannel.slice(0, numberOfPostsVisibleInCurrentChannel);
|
||||
for (const post of posts) {
|
||||
if (post.user_id) {
|
||||
userIds.add(post.user_id);
|
||||
if (post.user_id && post.user_id !== currentUserId) {
|
||||
userIdsToFetchStatusFor.add(post.user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We also fetch for users who have DMs open with the current user
|
||||
const directShowPreferences = getDirectShowPreferences(state);
|
||||
for (const directShowPreference of directShowPreferences) {
|
||||
if (directShowPreference.value === 'true') {
|
||||
// This is the other user's id in the DM
|
||||
userIds.add(directShowPreference.name);
|
||||
userIdsToFetchStatusFor.add(directShowPreference.name);
|
||||
}
|
||||
}
|
||||
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
userIds.add(currentUserId);
|
||||
|
||||
dispatch(loadStatusesByIds(Array.from(userIds)));
|
||||
// Both the users in the DM list and recent posts constitute for all the visible users in the current channel
|
||||
const userIdsForStatus = Array.from(userIdsToFetchStatusFor);
|
||||
if (userIdsForStatus.length > 0) {
|
||||
dispatch(addUserIdsForStatusAndProfileFetchingPoll({userIdsForStatus}));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
@@ -39,13 +39,13 @@ import {
|
||||
getCustomEmojiForReaction,
|
||||
getPosts,
|
||||
getPostThread,
|
||||
getMentionsAndStatusesForPosts,
|
||||
getPostThreads,
|
||||
postDeleted,
|
||||
receivedNewPost,
|
||||
receivedPost,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
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 {
|
||||
getThread as fetchThread,
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
getCurrentChannelId,
|
||||
getRedirectChannelNameForTeam,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getGroup} from 'mattermost-redux/selectors/entities/groups';
|
||||
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
|
||||
dispatch(getPosts(currentChannelId));
|
||||
}
|
||||
dispatch(StatusActions.loadStatusesForChannelAndSidebar());
|
||||
|
||||
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
||||
if (enabledUserStatuses) {
|
||||
dispatch(StatusActions.addVisibleUsersInCurrentChannelToStatusPoll());
|
||||
}
|
||||
|
||||
const crtEnabled = isCollapsedThreadsEnabled(state);
|
||||
dispatch(TeamActions.getMyTeamUnreads(crtEnabled, true));
|
||||
@@ -702,8 +707,7 @@ export function handleNewPostEvent(msg) {
|
||||
}
|
||||
|
||||
myDispatch(handleNewPost(post, msg));
|
||||
|
||||
getMentionsAndStatusesForPosts([post], myDispatch, myGetState);
|
||||
myDispatch(batchFetchStatusesProfilesGroupsFromPosts([post]));
|
||||
|
||||
// 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
|
||||
@@ -746,9 +750,7 @@ export function handleNewPostEvents(queue) {
|
||||
|
||||
// Load the posts' threads
|
||||
myDispatch(getPostThreads(posts));
|
||||
|
||||
// And any other data needed for them
|
||||
getMentionsAndStatusesForPosts(posts, myDispatch, myGetState);
|
||||
myDispatch(batchFetchStatusesProfilesGroupsFromPosts(posts));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -764,7 +766,7 @@ export function handlePostEditEvent(msg) {
|
||||
const crtEnabled = isCollapsedThreadsEnabled(getState());
|
||||
dispatch(receivedPost(post, crtEnabled));
|
||||
|
||||
getMentionsAndStatusesForPosts([post], dispatch, getState);
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts([post]));
|
||||
}
|
||||
|
||||
async function handlePostDeleteEvent(msg) {
|
||||
@@ -1141,7 +1143,7 @@ export async function handleUserUpdatedEvent(msg) {
|
||||
|
||||
if (currentUser.id === user.id) {
|
||||
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({
|
||||
type: UserTypes.RECEIVED_ME,
|
||||
data: user,
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
import {UserTypes, CloudTypes} from 'mattermost-redux/action_types';
|
||||
import {getGroup} from 'mattermost-redux/actions/groups';
|
||||
import {
|
||||
getMentionsAndStatusesForPosts,
|
||||
getPostThreads,
|
||||
receivedNewPost,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
||||
import {getUser} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {handleNewPost} from 'actions/post_actions';
|
||||
@@ -44,6 +44,11 @@ jest.mock('mattermost-redux/actions/posts', () => ({
|
||||
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.requireActual('mattermost-redux/actions/groups'),
|
||||
getGroup: jest.fn(() => ({type: 'RECEIVED_GROUP'})),
|
||||
@@ -488,8 +493,8 @@ describe('handleNewPostEvent', () => {
|
||||
};
|
||||
|
||||
testStore.dispatch(handleNewPostEvent(msg));
|
||||
expect(getMentionsAndStatusesForPosts).toHaveBeenCalledWith([post], expect.anything(), expect.anything());
|
||||
expect(handleNewPost).toHaveBeenCalledWith(post, msg);
|
||||
expect(batchFetchStatusesProfilesGroupsFromPosts).toHaveBeenCalledWith([post]);
|
||||
});
|
||||
|
||||
test('should set other user to online', () => {
|
||||
@@ -609,18 +614,17 @@ describe('handleNewPostEvents', () => {
|
||||
|
||||
testStore.dispatch(handleNewPostEvents(queue));
|
||||
|
||||
expect(testStore.getActions()).toEqual([
|
||||
{
|
||||
meta: {batch: true},
|
||||
payload: posts.map((post) => receivedNewPost(post, false)),
|
||||
type: 'BATCHING_REDUCER.BATCH',
|
||||
},
|
||||
{
|
||||
type: 'GET_THREADS_FOR_POSTS',
|
||||
},
|
||||
]);
|
||||
expect(testStore.getActions()[0]).toEqual({
|
||||
type: 'BATCHING_REDUCER.BATCH',
|
||||
meta: {batch: true},
|
||||
payload: posts.map((post) => receivedNewPost(post, false)),
|
||||
});
|
||||
expect(testStore.getActions()[1]).toEqual({
|
||||
type: 'GET_THREADS_FOR_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'),
|
||||
),
|
||||
},
|
||||
{
|
||||
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',
|
||||
key: 'TeamSettings.ExperimentalPrimaryTeam',
|
||||
|
||||
@@ -25,7 +25,7 @@ jest.mock('components/product_notices_modal', () => () => <div/>);
|
||||
jest.mock('plugins/pluggable', () => () => <div/>);
|
||||
|
||||
jest.mock('actions/status_actions', () => ({
|
||||
loadStatusesForChannelAndSidebar: jest.fn().mockImplementation(() => () => {}),
|
||||
addVisibleUsersInCurrentChannelToStatusPoll: jest.fn().mockImplementation(() => () => {}),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/general', () => ({
|
||||
@@ -46,7 +46,7 @@ describe('ChannelController', () => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('dispatches loadStatusesForChannelAndSidebar when enableUserStatuses is true', () => {
|
||||
it('dispatches addVisibleUsersInCurrentChannelToStatusPoll when enableUserStatuses is true', () => {
|
||||
mockState.entities.general.config.EnableUserStatuses = 'true';
|
||||
const store = mockStore(mockState);
|
||||
|
||||
@@ -60,10 +60,10 @@ describe('ChannelController', () => {
|
||||
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);
|
||||
mockState.entities.general.config.EnableUserStatuses = 'false';
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('ChannelController', () => {
|
||||
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 {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {cleanUpStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/status_profile_polling';
|
||||
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 LoadingScreen from 'components/loading_screen';
|
||||
@@ -40,6 +41,10 @@ export default function ChannelController(props: Props) {
|
||||
|
||||
return () => {
|
||||
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;
|
||||
if (enabledUserStatuses) {
|
||||
loadStatusesIntervalId = setInterval(() => {
|
||||
dispatch(loadStatusesForChannelAndSidebar());
|
||||
dispatch(addVisibleUsersInCurrentChannelToStatusPoll());
|
||||
}, Constants.STATUS_INTERVAL);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(loadStatusesIntervalId);
|
||||
};
|
||||
}, [dispatch, enabledUserStatuses]);
|
||||
}, [enabledUserStatuses]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/prefere
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
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 LocalStorageStore from 'stores/local_storage_store';
|
||||
|
||||
@@ -40,11 +40,8 @@ export function initializeTeam(team: Team): ActionFuncAsync<Team, GlobalState> {
|
||||
}
|
||||
|
||||
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
||||
|
||||
if (enabledUserStatuses) {
|
||||
// This is the first time in the pool of user statuses that we request,
|
||||
// subsequent requests will be done via setInterval at channel_controller.tsx
|
||||
dispatch(loadStatusesForChannelAndSidebar());
|
||||
dispatch(addVisibleUsersInCurrentChannelToStatusPoll());
|
||||
}
|
||||
|
||||
const license = getLicense(state);
|
||||
|
||||
@@ -989,6 +989,8 @@
|
||||
"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.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.example": "E.g.: \"300\"",
|
||||
"admin.experimental.userStatusAwayTimeout.title": "User Status Away Timeout:",
|
||||
|
||||
@@ -19,7 +19,6 @@ export default keyMirror({
|
||||
|
||||
GET_POSTS_SUCCESS: null,
|
||||
GET_POSTS_FAILURE: null,
|
||||
GET_POSTS_SINCE_SUCCESS: null,
|
||||
|
||||
GET_POST_THREAD_WITH_RETRY_ATTEMPT: null,
|
||||
GET_POSTS_WITH_RETRY_ATTEMPT: null,
|
||||
|
||||
@@ -25,6 +25,12 @@ import {Preferences, Posts, RequestStatus} from '../constants';
|
||||
|
||||
const OK_RESPONSE = {status: 'OK'};
|
||||
|
||||
jest.mock('mattermost-redux/actions/status_profile_polling', () => ({
|
||||
batchFetchStatusesProfilesGroupsFromPosts: jest.fn(() => {
|
||||
return {type: ''};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Actions.Posts', () => {
|
||||
let store = configureStore();
|
||||
beforeAll(() => {
|
||||
@@ -1633,6 +1639,7 @@ describe('getPostThreads', () => {
|
||||
users: {
|
||||
currentUserId: 'currentUserId',
|
||||
statuses: {},
|
||||
profiles: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
@@ -1671,15 +1678,15 @@ describe('getPostThreads', () => {
|
||||
|
||||
await testStore.dispatch(Actions.getPostThreads([comment]));
|
||||
|
||||
expect(testStore.getActions()[1].payload[0].type).toEqual('RECEIVED_POSTS');
|
||||
expect(testStore.getActions()[1].payload[0].data.posts).toEqual({
|
||||
expect(testStore.getActions()[0].payload[0].type).toEqual('RECEIVED_POSTS');
|
||||
expect(testStore.getActions()[0].payload[0].data.posts).toEqual({
|
||||
[post1.id]: post1,
|
||||
[comment.id]: comment,
|
||||
});
|
||||
|
||||
expect(testStore.getActions()[1].payload[1].type).toEqual('RECEIVED_POSTS_IN_THREAD');
|
||||
expect(testStore.getActions()[1].payload[1].rootId).toEqual(post1.id);
|
||||
expect(testStore.getActions()[1].payload[1].data.posts).toEqual({
|
||||
expect(testStore.getActions()[0].payload[1].type).toEqual('RECEIVED_POSTS_IN_THREAD');
|
||||
expect(testStore.getActions()[0].payload[1].rootId).toEqual(post1.id);
|
||||
expect(testStore.getActions()[0].payload[1].data.posts).toEqual({
|
||||
[post1.id]: post1,
|
||||
[comment.id]: comment,
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
deletePreferences,
|
||||
savePreferences,
|
||||
} from 'mattermost-redux/actions/preferences';
|
||||
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
||||
import {decrementThreadCounts} from 'mattermost-redux/actions/threads';
|
||||
import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||
import {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE} from 'mattermost-redux/client';
|
||||
@@ -153,7 +154,6 @@ export function getPost(postId: string): ActionFuncAsync<Post> {
|
||||
|
||||
try {
|
||||
post = await Client4.getPost(postId);
|
||||
getMentionsAndStatusesForPosts([post], dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch({type: PostTypes.GET_POSTS_FAILURE, error});
|
||||
@@ -161,12 +161,8 @@ export function getPost(postId: string): ActionFuncAsync<Post> {
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
receivedPost(post, crtEnabled),
|
||||
{
|
||||
type: PostTypes.GET_POSTS_SUCCESS,
|
||||
},
|
||||
]));
|
||||
dispatch(receivedPost(post, crtEnabled));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts([post]));
|
||||
|
||||
return {data: post};
|
||||
};
|
||||
@@ -709,16 +705,15 @@ async function getPaginatedPostThread(rootId: string, options: FetchPaginatedThr
|
||||
|
||||
export function getPostThread(rootId: string, fetchThreads = true): ActionFuncAsync<PostList> {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: PostTypes.GET_POST_THREAD_REQUEST});
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
const state = getState();
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
|
||||
|
||||
let posts;
|
||||
try {
|
||||
posts = await getPaginatedPostThread(rootId, {fetchThreads, collapsedThreads: collapsedThreadsEnabled});
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch({type: PostTypes.GET_POST_THREAD_FAILURE, error});
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
@@ -726,11 +721,12 @@ export function getPostThread(rootId: string, fetchThreads = true): ActionFuncAs
|
||||
dispatch(batchActions([
|
||||
receivedPosts(posts),
|
||||
receivedPostsInThread(posts, rootId),
|
||||
{
|
||||
type: PostTypes.GET_POST_THREAD_SUCCESS,
|
||||
},
|
||||
]));
|
||||
|
||||
if (enabledUserStatuses) {
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||
}
|
||||
|
||||
return {data: posts};
|
||||
};
|
||||
}
|
||||
@@ -756,7 +752,6 @@ export function getNewestPostThread(rootId: string): ActionFuncAsync {
|
||||
let posts;
|
||||
try {
|
||||
posts = await getPaginatedPostThread(rootId, options);
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch({type: PostTypes.GET_POST_THREAD_FAILURE, error});
|
||||
@@ -767,10 +762,8 @@ export function getNewestPostThread(rootId: string): ActionFuncAsync {
|
||||
dispatch(batchActions([
|
||||
receivedPosts(posts),
|
||||
receivedPostsInThread(posts, rootId),
|
||||
{
|
||||
type: PostTypes.GET_POST_THREAD_SUCCESS,
|
||||
},
|
||||
]));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||
|
||||
return {data: posts};
|
||||
};
|
||||
@@ -782,7 +775,6 @@ export function getPosts(channelId: string, page = 0, perPage = Posts.POST_CHUNK
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
try {
|
||||
posts = await Client4.getPosts(channelId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
@@ -793,6 +785,7 @@ export function getPosts(channelId: string, page = 0, perPage = Posts.POST_CHUNK
|
||||
receivedPosts(posts),
|
||||
receivedPostsInChannel(posts, channelId, page === 0, posts.prev_post_id === ''),
|
||||
]));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.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> {
|
||||
return async (dispatch, getState) => {
|
||||
const shouldLoadRecent = getUnreadScrollPositionPreference(getState()) === Preferences.UNREAD_SCROLL_POSITION_START_FROM_NEWEST;
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
const userId = getCurrentUserId(getState());
|
||||
const state = getState();
|
||||
const shouldLoadRecent = getUnreadScrollPositionPreference(state) === Preferences.UNREAD_SCROLL_POSITION_START_FROM_NEWEST;
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const userId = getCurrentUserId(state);
|
||||
|
||||
let posts;
|
||||
let recentPosts;
|
||||
try {
|
||||
@@ -811,29 +806,29 @@ export function getPostsUnread(channelId: string, fetchThreads = true, collapsed
|
||||
if (posts.next_post_id && shouldLoadRecent) {
|
||||
recentPosts = await Client4.getPosts(channelId, 0, Posts.POST_CHUNK_SIZE / 2, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
}
|
||||
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
const recentPostsActions = recentPosts ? [
|
||||
receivedPosts(recentPosts),
|
||||
receivedPostsInChannel(recentPosts, channelId, recentPosts.next_post_id === '', recentPosts.prev_post_id === ''),
|
||||
] : [];
|
||||
|
||||
dispatch(batchActions([
|
||||
receivedPosts(posts),
|
||||
const actions: AnyAction[] = [
|
||||
{
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: posts,
|
||||
channelId,
|
||||
},
|
||||
receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''),
|
||||
...recentPostsActions,
|
||||
]));
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: posts,
|
||||
channelId,
|
||||
});
|
||||
];
|
||||
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};
|
||||
};
|
||||
@@ -845,7 +840,6 @@ export function getPostsSince(channelId: string, since: number, fetchThreads = t
|
||||
try {
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
posts = await Client4.getPostsSince(channelId, since, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
@@ -855,10 +849,8 @@ export function getPostsSince(channelId: string, since: number, fetchThreads = t
|
||||
dispatch(batchActions([
|
||||
receivedPosts(posts),
|
||||
receivedPostsSince(posts, channelId),
|
||||
{
|
||||
type: PostTypes.GET_POSTS_SINCE_SUCCESS,
|
||||
},
|
||||
]));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||
|
||||
return {data: posts};
|
||||
};
|
||||
@@ -870,7 +862,6 @@ export function getPostsBefore(channelId: string, postId: string, page = 0, perP
|
||||
try {
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
posts = await Client4.getPostsBefore(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
@@ -881,6 +872,7 @@ export function getPostsBefore(channelId: string, postId: string, page = 0, perP
|
||||
receivedPosts(posts),
|
||||
receivedPostsBefore(posts, channelId, postId, posts.prev_post_id === ''),
|
||||
]));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.posts));
|
||||
|
||||
return {data: posts};
|
||||
};
|
||||
@@ -892,7 +884,6 @@ export function getPostsAfter(channelId: string, postId: string, page = 0, perPa
|
||||
try {
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
posts = await Client4.getPostsAfter(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
@@ -903,6 +894,7 @@ export function getPostsAfter(channelId: string, postId: string, page = 0, perPa
|
||||
receivedPosts(posts),
|
||||
receivedPostsAfter(posts, channelId, postId, posts.next_post_id === ''),
|
||||
]));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.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,
|
||||
};
|
||||
|
||||
getMentionsAndStatusesForPosts(posts.posts, dispatch, getState);
|
||||
|
||||
dispatch(batchActions([
|
||||
receivedPosts(posts),
|
||||
receivedPostsInChannel(posts, channelId, after.next_post_id === '', before.prev_post_id === ''),
|
||||
]));
|
||||
dispatch(batchFetchStatusesProfilesGroupsFromPosts(posts.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: []};
|
||||
}
|
||||
|
||||
let recievedStatuses: UserStatus[];
|
||||
let receivedStatuses: UserStatus[];
|
||||
try {
|
||||
recievedStatuses = await Client4.getStatusesByIds(userIds);
|
||||
receivedStatuses = await Client4.getStatusesByIds(userIds);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
@@ -609,11 +609,11 @@ export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncA
|
||||
const isManualStatuses: Record<UserProfile['id'], UserStatus['manual']> = {};
|
||||
const lastActivity: Record<UserProfile['id'], UserStatus['last_activity_at']> = {};
|
||||
|
||||
for (const recievedStatus of recievedStatuses) {
|
||||
statuses[recievedStatus.user_id] = recievedStatus?.status ?? '';
|
||||
dndEndTimes[recievedStatus.user_id] = recievedStatus?.dnd_end_time ?? 0;
|
||||
isManualStatuses[recievedStatus.user_id] = recievedStatus?.manual ?? false;
|
||||
lastActivity[recievedStatus.user_id] = recievedStatus?.last_activity_at ?? 0;
|
||||
for (const receivedStatus of receivedStatuses) {
|
||||
statuses[receivedStatus.user_id] = receivedStatus?.status ?? '';
|
||||
dndEndTimes[receivedStatus.user_id] = receivedStatus?.dnd_end_time ?? 0;
|
||||
isManualStatuses[receivedStatus.user_id] = receivedStatus?.manual ?? false;
|
||||
lastActivity[receivedStatus.user_id] = receivedStatus?.last_activity_at ?? 0;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
@@ -637,7 +637,7 @@ export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncA
|
||||
'BATCHING_STATUSES',
|
||||
));
|
||||
|
||||
return {data: recievedStatuses};
|
||||
return {data: receivedStatuses};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -122,3 +122,16 @@ export const getGiphyFetchInstance: (state: GlobalState) => GiphyFetch | 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;
|
||||
ServiceEnvironment: string;
|
||||
UniqueEmojiReactionLimitPerPost: string;
|
||||
UsersStatusAndProfileFetchingPollIntervalMilliseconds: string;
|
||||
};
|
||||
|
||||
export type License = {
|
||||
|
||||
Ссылка в новой задаче
Block a user