mattermost-mobile-upstream/app/actions/websocket/index.ts
Mattermost Build f8a177ca48
Mm 70299 channel labels display phase 1 (#10113) (#10171)
* MM-70299 - display channel attributes in header, info and banner

* cover and expand channel attributes feature with UI and E2E tests

* Implement channel attributes lock and update related tests and API methods

* Enhance error logging and improve type definitions for channel attributes

* Handle error from setAccessControlGroupId in fetchAccessControlAttributeFields

* Refactor fetchAccessControlAttributeFields to include system ID handling and improve batch record persistence

* Address code review feedback: extract feature-flag constants, move DB-only helpers to query/local layers, refactor attribute chip styles, fix colour spelling, and add test coverage for value pruning and subtitle component

* Fix error handling in removeStoredFields function and delete obsolete end-to-end test file

* Add tests for removeStoredFields function and enhance property queries

* Fix removeStoredFields to recover group ID from classification anchor and clean all owned fields

* Fix batchRecords silently swallowing write errors in fetchAccessControlAttributeFields

* Remove unused visibility check for ChannelAttributeLabels in e2e tests

* Fix classification fetch cache and channel-attributes E2E lock contention

Do not stamp the classification banner cache when both access-control flags
are off, so turning a flag back on after a reload refetches fields.

Share the classification lock with the channel-attributes Detox suite and
stop unsetting ClassificationMarkings. Skip flag-on tests when the server
cannot enable ChannelAttributes.



* Scope access_control field prune to owned object types

handlePropertyFields(groupId) deletes every field in the group. The fetch
only returns system and channel fields, so same-group user/session rows
were removed on each refresh. Prune only system and channel fields, matching
removeStoredFields.



* fail channel attributes tests when feature enablement fails instead of silently reporting them as passed

* Compose all channel attribute values in banners with template resolution, mixed field support, and matching web colors

---------




(cherry picked from commit 54731fa6ac)

Co-authored-by: Pablo Vélez <pablovv2016@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
2026-09-18 08:10:30 +03:00

200 lines
8.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {markChannelAsViewed} from '@actions/local/channel';
import {autoCacheCleanup} from '@actions/local/ephemeral_mode/cleanup';
import {dataRetentionCleanup, expiredBoRPostCleanup, performVacuum} from '@actions/local/systems';
import {markChannelAsRead} from '@actions/remote/channel';
import {fetchAccessControlAttributeFields, fetchChannelAttributeValues} from '@actions/remote/classification';
import {
entry,
handleEntryAfterLoadNavigation,
setExtraSessionProps,
} from '@actions/remote/entry/common';
import {deferredAppEntryActions} from '@actions/remote/entry/deferred';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {openAllUnreadChannels} from '@actions/remote/preference';
import {autoUpdateTimezone} from '@actions/remote/user';
import {checkIsAgentsPluginEnabled} from '@agents/actions/remote/agents_status';
import {handleAgentsReconnect} from '@agents/actions/websocket/reconnect';
import {loadConfigAndCalls} from '@calls/actions/calls';
import {isSupportedServerCalls} from '@calls/utils';
import {Screens} from '@constants';
import DatabaseManager from '@database/manager';
import AppsManager from '@managers/apps_manager';
import SessionAttributesManager from '@managers/session_attributes_manager';
import {handlePlaybookReconnect} from '@playbooks/actions/websocket/reconnect';
import {getActiveServerUrl} from '@queries/app/servers';
import {getLastPostInThread} from '@queries/servers/post';
import {
getConfig,
getCurrentChannelId,
getCurrentTeamId,
getLicense,
getLastFullSync,
setLastFullSync,
} from '@queries/servers/system';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {NavigationStore} from '@store/navigation_store';
import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {logDebug, logInfo} from '@utils/log';
export async function handleFirstConnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) {
setExtraSessionProps(serverUrl, groupLabel);
autoUpdateTimezone(serverUrl, groupLabel);
return doReconnect(serverUrl, groupLabel);
}
export async function handleReconnect(serverUrl: string, groupLabel: BaseRequestGroupLabel = 'WebSocket Reconnect') {
return doReconnect(serverUrl, groupLabel);
}
async function doReconnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return new Error('cannot find server database');
}
const appDatabase = DatabaseManager.appDatabase?.database;
if (!appDatabase) {
return new Error('cannot find app database');
}
const {database} = operator;
// Guards against RUNNINGBOARD 0xdead10cc if the app backgrounds mid-sync.
const activityToken = await RNUtils.beginDatabaseActivity(serverUrl, 'doReconnect');
try {
await SessionAttributesManager.refreshManifest(serverUrl);
const lastFullSync = await getLastFullSync(database);
const now = Date.now();
const currentTeamId = await getCurrentTeamId(database);
const currentChannelId = await getCurrentChannelId(database);
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastFullSync, groupLabel);
if ('error' in entryData) {
return entryData.error;
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData, gmConverted} = entryData;
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId, gmConverted);
const dt = Date.now();
if (models?.length) {
await operator.batchRecords(models, 'doReconnect');
}
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
await fetchPostDataIfNeeded(serverUrl, groupLabel);
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!;
const license = await getLicense(database);
const config = await getConfig(database);
handlePlaybookReconnect(serverUrl);
handleAgentsReconnect(serverUrl);
if (isSupportedServerCalls(config?.Version)) {
loadConfigAndCalls(serverUrl, currentUserId, groupLabel);
}
checkIsAgentsPluginEnabled(serverUrl);
fetchAccessControlAttributeFields(serverUrl, true);
// Values may have changed while the socket was down, and the events that
// would have reported it are gone. Dropping the per-channel dedupe makes
// the next visit to each channel refetch them — but the channel already on
// screen is never re-entered, so it is refetched explicitly or it would
// keep showing what it had when the connection dropped.
EphemeralStore.clearChannelAttributeValuesSynced(serverUrl);
if (currentChannelId) {
fetchChannelAttributeValues(serverUrl, currentChannelId);
}
await deferredAppEntryActions(serverUrl, lastFullSync, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, meData, initialTeamId, undefined, groupLabel);
await setLastFullSync(operator, now);
openAllUnreadChannels(serverUrl, groupLabel);
doCleanup(serverUrl);
AppsManager.refreshAppBindings(serverUrl, groupLabel);
return undefined;
} finally {
setTeamLoading(serverUrl, false);
if (activityToken) {
RNUtils.endDatabaseActivity(activityToken);
}
}
}
async function doCleanup(serverUrl: string) {
const dataRetention = await dataRetentionCleanup(serverUrl);
const autoCache = await autoCacheCleanup(serverUrl);
await expiredBoRPostCleanup(serverUrl);
const dataRetentionRan = !dataRetention.skipped && !dataRetention.error;
const autoCacheRan = !autoCache.skipped && !autoCache.error;
if (dataRetentionRan || autoCacheRan) {
await performVacuum(serverUrl);
}
}
async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: RequestGroupLabel) {
try {
const isActiveServer = (await getActiveServerUrl()) === serverUrl;
if (!isActiveServer) {
return;
}
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentChannelId = await getCurrentChannelId(database);
const isCRTEnabled = await getIsCRTEnabled(database);
const mountedScreens = NavigationStore.getScreensInStack();
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
const isThreadScreenMounted = mountedScreens.includes(Screens.THREAD);
const tabletDevice = isTablet();
if (isCRTEnabled && isThreadScreenMounted) {
// Fetch new posts in the thread only when CRT is enabled,
// for non-CRT fetchPostsForChannel includes posts in the thread
const rootId = EphemeralStore.getCurrentThreadId();
if (rootId) {
const lastPost = await getLastPostInThread(database, rootId);
if (lastPost) {
if (lastPost) {
const options: FetchPaginatedThreadOptions = {};
options.fromCreateAt = lastPost.createAt;
options.fromPost = lastPost.id;
options.direction = 'down';
await fetchPostThread(serverUrl, rootId, options, false, groupLabel);
}
}
}
}
if (currentChannelId && (isChannelScreenMounted || tabletDevice)) {
await fetchPostsForChannel(serverUrl, currentChannelId, false, false, groupLabel);
markChannelAsRead(serverUrl, currentChannelId, false, groupLabel);
if (!EphemeralStore.wasNotificationTapped()) {
markChannelAsViewed(serverUrl, currentChannelId, true);
}
EphemeralStore.setNotificationTapped(false);
}
} catch (error) {
logDebug('could not fetch needed post after WS reconnect', error);
}
}