mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
* 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. Co-authored-by: mattermost-code <matty-code@mattermost.com> * 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. Co-authored-by: mattermost-code <matty-code@mattermost.com> * 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 --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: mattermost-code <matty-code@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
562 lines
20 KiB
TypeScript
562 lines
20 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {DeviceEventEmitter} from 'react-native';
|
|
import {BehaviorSubject} from 'rxjs';
|
|
|
|
import {Events} from '@constants';
|
|
import {CLASSIFICATION_BANNER_CACHE_TTL} from '@constants/classification';
|
|
import {getDefaultThemeByAppearance} from '@context/theme';
|
|
import {toMilliseconds} from '@utils/datetime';
|
|
|
|
const TIME_TO_CLEAR_WEBSOCKET_ACTIONS = toMilliseconds({seconds: 30});
|
|
const TIME_TO_CLEAR_RECENTLY_UNSAVED_SAVED_POSTS = toMilliseconds({minutes: 2});
|
|
|
|
class EphemeralStoreSingleton {
|
|
private themeSubject = new BehaviorSubject<Theme>(getDefaultThemeByAppearance());
|
|
creatingChannel = false;
|
|
creatingDMorGMTeammates: string[] = [];
|
|
|
|
noticeShown = new Set<string>();
|
|
|
|
private pushProxyVerification: {[serverUrl: string]: string | undefined} = {};
|
|
private canJoinOtherTeams: {[serverUrl: string]: BehaviorSubject<boolean>} = {};
|
|
|
|
private loadingMessagesForChannel: {[serverUrl: string]: Set<string>} = {};
|
|
|
|
private websocketEditingPost: {[serverUrl: string]: {[id: string]: {post: Post; timeout: NodeJS.Timeout} | undefined} | undefined} = {};
|
|
private websocketRemovingPost: {[serverUrl: string]: Set<string> | undefined} = {};
|
|
private recentlyUnsavedSavedPosts: {[serverUrl: string]: {[postId: string]: NodeJS.Timeout} | undefined} = {};
|
|
private recentlyUnsavedSavedPostsState: {[serverUrl: string]: BehaviorSubject<Set<string>> | undefined} = {};
|
|
private recentlyUnsavedSavedPostSubjects: {[serverUrl: string]: {[postId: string]: BehaviorSubject<boolean>} | undefined} = {};
|
|
|
|
// As of today, the server sends a duplicated event to add the user to the team.
|
|
// If we do not handle this, this ends up showing some errors in the database, apart
|
|
// of the extra computation time. We use this to track the events that are being handled
|
|
// and make sure we only handle one.
|
|
private addingTeam = new Set<string>();
|
|
private joiningChannels = new Set<string>();
|
|
private leavingChannels = new Set<string>();
|
|
private archivingChannels = new Set<string>();
|
|
private convertingChannels = new Set<string>();
|
|
private switchingToChannel = new Set<string>();
|
|
private acknowledgingPost = new Set<string>();
|
|
private unacknowledgingPost = new Set<string>();
|
|
private currentThreadId = '';
|
|
private currentChannelOldestVisibleCreateAt: number | undefined = undefined;
|
|
private currentFileViewerPostId = '';
|
|
private currentPlaybookRunId = '';
|
|
private notificationTapped = false;
|
|
private enablingCRT = false;
|
|
|
|
// There are some corner cases where the react context is not loaded (and therefore
|
|
// launch will be called) but the notification callbacks are registered. This is used
|
|
// so the notification is processed only once (preferably on launch).
|
|
private processingNotification = '';
|
|
|
|
// This is used to track the channels that have their playbooks synced with the server.
|
|
// This is used to avoid fetching the playbooks for the same channel multiple times.
|
|
// It is cleared any time the connection with the server is lost.
|
|
private channelPlaybooksSynced: {[serverUrl: string]: Set<string>} = {};
|
|
|
|
private managedCategoryPropertyIds: {[serverUrl: string]: {groupId: string; fieldId: string} | undefined} = {};
|
|
|
|
// Track when the classification banner fields were last fetched per server, so
|
|
// the global banner can skip redundant fetches within the TTL. In-memory only
|
|
// (cleared on app restart) and cleared per server on logout.
|
|
private classificationBannerFetchedAt: {[serverUrl: string]: number | undefined} = {};
|
|
|
|
// Track classification option ids we've already forced a field refresh for, so an
|
|
// option that is genuinely gone server-side does not trigger an infinite re-fetch loop.
|
|
private classificationFieldSyncAttempted: {[serverUrl: string]: Set<string>} = {};
|
|
|
|
// Channels whose attribute values have been fetched from the server, so a
|
|
// repeated channel switch does not refetch. Values stay fresh afterwards via
|
|
// websocket events; the set is cleared when the connection drops.
|
|
private channelAttributeValuesSynced: {[serverUrl: string]: Set<string>} = {};
|
|
|
|
// Track how many translations are being executed at the same time on the channel.
|
|
// We limit this to avoid overwhelming the device.
|
|
private runningTranslations = new Set<string>();
|
|
|
|
addRunningTranslation = (postId: string) => {
|
|
this.runningTranslations.add(postId);
|
|
};
|
|
|
|
removeRunningTranslation = (postId: string) => {
|
|
this.runningTranslations.delete(postId);
|
|
};
|
|
|
|
totalRunningTranslations = () => {
|
|
return this.runningTranslations.size;
|
|
};
|
|
|
|
// Track files that have been rejected by plugins (transient state)
|
|
// Maps file ID to rejection reason
|
|
private rejectedFiles = new Map<string, string>();
|
|
|
|
setProcessingNotification = (v: string) => {
|
|
this.processingNotification = v;
|
|
};
|
|
getProcessingNotification = () => this.processingNotification;
|
|
|
|
addLoadingMessagesForChannel = (serverUrl: string, channelId: string) => {
|
|
if (!this.loadingMessagesForChannel[serverUrl]) {
|
|
this.loadingMessagesForChannel[serverUrl] = new Set();
|
|
}
|
|
|
|
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, {serverUrl, channelId, value: true});
|
|
this.loadingMessagesForChannel[serverUrl].add(channelId);
|
|
};
|
|
|
|
stopLoadingMessagesForChannel = (serverUrl: string, channelId: string) => {
|
|
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, {serverUrl, channelId, value: false});
|
|
this.loadingMessagesForChannel[serverUrl]?.delete(channelId);
|
|
};
|
|
|
|
isLoadingMessagesForChannel = (serverUrl: string, channelId: string) => {
|
|
return Boolean(this.loadingMessagesForChannel[serverUrl]?.has(channelId));
|
|
};
|
|
|
|
// Ephemeral control for out of order websocket events
|
|
addEditingPost = (serverUrl: string, post: Post) => {
|
|
if (this.websocketRemovingPost[serverUrl]?.has(post.id)) {
|
|
return;
|
|
}
|
|
|
|
const lastEdit = this.websocketEditingPost[serverUrl]?.[post.id];
|
|
if (lastEdit && post.edit_at < lastEdit.post.update_at) {
|
|
return;
|
|
}
|
|
|
|
if (!this.websocketEditingPost[serverUrl]) {
|
|
this.websocketEditingPost[serverUrl] = {};
|
|
}
|
|
const serverEditing = this.websocketEditingPost[serverUrl]!;
|
|
|
|
if (lastEdit?.timeout) {
|
|
clearTimeout(lastEdit.timeout);
|
|
}
|
|
|
|
const timeout = setTimeout(() => {
|
|
delete serverEditing[post.id];
|
|
}, TIME_TO_CLEAR_WEBSOCKET_ACTIONS);
|
|
|
|
serverEditing[post.id] = {post, timeout};
|
|
};
|
|
|
|
addRemovingPost = (serverUrl: string, postId: string) => {
|
|
if (this.websocketRemovingPost[serverUrl]?.has(postId)) {
|
|
return;
|
|
}
|
|
|
|
if (this.websocketEditingPost[serverUrl]?.[postId]) {
|
|
clearTimeout(this.websocketEditingPost[serverUrl]![postId]!.timeout);
|
|
delete this.websocketEditingPost[serverUrl]![postId];
|
|
}
|
|
|
|
if (!this.websocketRemovingPost[serverUrl]) {
|
|
this.websocketRemovingPost[serverUrl] = new Set();
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.websocketRemovingPost[serverUrl]?.delete(postId);
|
|
}, TIME_TO_CLEAR_WEBSOCKET_ACTIONS);
|
|
|
|
this.websocketRemovingPost[serverUrl]?.add(postId);
|
|
};
|
|
|
|
getLastPostWebsocketEvent = (serverUrl: string, postId: string) => {
|
|
if (this.websocketRemovingPost[serverUrl]?.has(postId)) {
|
|
return {deleted: true, post: undefined};
|
|
}
|
|
|
|
if (this.websocketEditingPost[serverUrl]?.[postId]) {
|
|
return {deleted: false, post: this.websocketEditingPost[serverUrl]![postId]!.post};
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
getRecentlyUnsavedSavedPostSubject = (serverUrl: string, postId: string) => {
|
|
if (!this.recentlyUnsavedSavedPostSubjects[serverUrl]) {
|
|
this.recentlyUnsavedSavedPostSubjects[serverUrl] = {};
|
|
}
|
|
|
|
const serverSubjects = this.recentlyUnsavedSavedPostSubjects[serverUrl]!;
|
|
if (!serverSubjects[postId]) {
|
|
serverSubjects[postId] = new BehaviorSubject(false);
|
|
}
|
|
|
|
return serverSubjects[postId]!;
|
|
};
|
|
|
|
observeRecentlyUnsavedSavedPost = (serverUrl: string, postId: string) => {
|
|
return this.getRecentlyUnsavedSavedPostSubject(serverUrl, postId).asObservable();
|
|
};
|
|
|
|
getRecentlyUnsavedSavedPostsSubject = (serverUrl: string) => {
|
|
if (!this.recentlyUnsavedSavedPostsState[serverUrl]) {
|
|
this.recentlyUnsavedSavedPostsState[serverUrl] = new BehaviorSubject(new Set());
|
|
}
|
|
|
|
return this.recentlyUnsavedSavedPostsState[serverUrl]!;
|
|
};
|
|
|
|
observeRecentlyUnsavedSavedPosts = (serverUrl: string) => {
|
|
return this.getRecentlyUnsavedSavedPostsSubject(serverUrl).asObservable();
|
|
};
|
|
|
|
addRecentlyUnsavedSavedPost = (serverUrl: string, postId: string) => {
|
|
if (!this.recentlyUnsavedSavedPosts[serverUrl]) {
|
|
this.recentlyUnsavedSavedPosts[serverUrl] = {};
|
|
}
|
|
|
|
const serverPosts = this.recentlyUnsavedSavedPosts[serverUrl]!;
|
|
const subject = this.getRecentlyUnsavedSavedPostSubject(serverUrl, postId);
|
|
const stateSubject = this.getRecentlyUnsavedSavedPostsSubject(serverUrl);
|
|
const existingTimeout = serverPosts[postId];
|
|
if (existingTimeout) {
|
|
clearTimeout(existingTimeout);
|
|
}
|
|
|
|
subject.next(true);
|
|
stateSubject.next(new Set([...stateSubject.value, postId]));
|
|
serverPosts[postId] = setTimeout(() => {
|
|
subject.next(false);
|
|
const nextState = new Set(stateSubject.value);
|
|
nextState.delete(postId);
|
|
stateSubject.next(nextState);
|
|
this.removeRecentlyUnsavedSavedPostSubject(serverUrl, postId);
|
|
delete serverPosts[postId];
|
|
if (!Object.keys(serverPosts).length) {
|
|
delete this.recentlyUnsavedSavedPosts[serverUrl];
|
|
}
|
|
}, TIME_TO_CLEAR_RECENTLY_UNSAVED_SAVED_POSTS);
|
|
};
|
|
|
|
private removeRecentlyUnsavedSavedPostSubject = (serverUrl: string, postId: string) => {
|
|
const serverSubjects = this.recentlyUnsavedSavedPostSubjects[serverUrl];
|
|
if (!serverSubjects?.[postId]) {
|
|
return;
|
|
}
|
|
|
|
serverSubjects[postId]!.complete();
|
|
delete serverSubjects[postId];
|
|
if (!Object.keys(serverSubjects).length) {
|
|
delete this.recentlyUnsavedSavedPostSubjects[serverUrl];
|
|
}
|
|
};
|
|
|
|
clearRecentlyUnsavedSavedPost = (serverUrl: string, postId: string) => {
|
|
this.recentlyUnsavedSavedPostSubjects[serverUrl]?.[postId]?.next(false);
|
|
const stateSubject = this.recentlyUnsavedSavedPostsState[serverUrl];
|
|
if (stateSubject?.value.has(postId)) {
|
|
const nextState = new Set(stateSubject.value);
|
|
nextState.delete(postId);
|
|
stateSubject.next(nextState);
|
|
}
|
|
|
|
const timeout = this.recentlyUnsavedSavedPosts[serverUrl]?.[postId];
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
delete this.recentlyUnsavedSavedPosts[serverUrl]![postId];
|
|
if (!Object.keys(this.recentlyUnsavedSavedPosts[serverUrl]!).length) {
|
|
delete this.recentlyUnsavedSavedPosts[serverUrl];
|
|
}
|
|
}
|
|
|
|
this.removeRecentlyUnsavedSavedPostSubject(serverUrl, postId);
|
|
};
|
|
|
|
isRecentlyUnsavedSavedPost = (serverUrl: string, postId: string) => {
|
|
return Boolean(this.recentlyUnsavedSavedPosts[serverUrl]?.[postId]);
|
|
};
|
|
|
|
// Ephemeral control when (un)archiving a channel locally
|
|
addArchivingChannel = (channelId: string) => {
|
|
this.archivingChannels.add(channelId);
|
|
};
|
|
|
|
isArchivingChannel = (channelId: string) => {
|
|
return this.archivingChannels.has(channelId);
|
|
};
|
|
|
|
removeArchivingChannel = (channelId: string) => {
|
|
this.archivingChannels.delete(channelId);
|
|
};
|
|
|
|
// Ephemeral control when converting a channel to private locally
|
|
addConvertingChannel = (channelId: string) => {
|
|
this.convertingChannels.add(channelId);
|
|
};
|
|
|
|
isConvertingChannel = (channelId: string) => {
|
|
return this.convertingChannels.has(channelId);
|
|
};
|
|
|
|
removeConvertingChannel = (channelId: string) => {
|
|
this.convertingChannels.delete(channelId);
|
|
};
|
|
|
|
// Ephemeral control when leaving a channel locally
|
|
addLeavingChannel = (channelId: string) => {
|
|
this.leavingChannels.add(channelId);
|
|
};
|
|
|
|
isLeavingChannel = (channelId: string) => {
|
|
return this.leavingChannels.has(channelId);
|
|
};
|
|
|
|
removeLeavingChannel = (channelId: string) => {
|
|
this.leavingChannels.delete(channelId);
|
|
};
|
|
|
|
// Ephemeral control when joining a channel locally
|
|
addJoiningChannel = (channelId: string) => {
|
|
this.joiningChannels.add(channelId);
|
|
};
|
|
|
|
isJoiningChannel = (channelId: string) => {
|
|
return this.joiningChannels.has(channelId);
|
|
};
|
|
|
|
removeJoiningChannel = (channelId: string) => {
|
|
this.joiningChannels.delete(channelId);
|
|
};
|
|
|
|
// Ephemeral control when adding a team locally
|
|
startAddingToTeam = (teamId: string) => {
|
|
this.addingTeam.add(teamId);
|
|
};
|
|
|
|
finishAddingToTeam = (teamId: string) => {
|
|
this.addingTeam.delete(teamId);
|
|
};
|
|
|
|
isAddingToTeam = (teamId: string) => {
|
|
return this.addingTeam.has(teamId);
|
|
};
|
|
|
|
// Ephemeral for push proxy state
|
|
setPushProxyVerificationState = (serverUrl: string, state: string) => {
|
|
this.pushProxyVerification[serverUrl] = state;
|
|
};
|
|
|
|
getPushProxyVerificationState = (serverUrl: string) => {
|
|
return this.pushProxyVerification[serverUrl];
|
|
};
|
|
|
|
// Ephemeral for the last viewed thread
|
|
getCurrentThreadId = () => {
|
|
return this.currentThreadId;
|
|
};
|
|
|
|
setCurrentThreadId = (id: string) => {
|
|
this.currentThreadId = id;
|
|
};
|
|
|
|
getCurrentChannelOldestVisibleCreateAt = () => this.currentChannelOldestVisibleCreateAt;
|
|
setCurrentChannelOldestVisibleCreateAt = (value: number | undefined) => {
|
|
this.currentChannelOldestVisibleCreateAt = value;
|
|
};
|
|
|
|
// Protects the backing post from eviction while a file viewer is open.
|
|
getCurrentFileViewerPostId = () => this.currentFileViewerPostId;
|
|
setCurrentFileViewerPostId = (id: string) => {
|
|
this.currentFileViewerPostId = id;
|
|
};
|
|
clearCurrentFileViewerPostId = () => {
|
|
this.currentFileViewerPostId = '';
|
|
};
|
|
|
|
// Protects the viewed playbook run from rolling cache eviction.
|
|
getCurrentPlaybookRunId = () => this.currentPlaybookRunId;
|
|
setCurrentPlaybookRunId = (id: string) => {
|
|
this.currentPlaybookRunId = id;
|
|
};
|
|
|
|
// Ephemeral control when (un)archiving a channel locally
|
|
addSwitchingToChannel = (channelId: string) => {
|
|
this.switchingToChannel.add(channelId);
|
|
};
|
|
|
|
isSwitchingToChannel = (channelId: string) => {
|
|
return this.switchingToChannel.has(channelId);
|
|
};
|
|
|
|
removeSwitchingToChannel = (channelId: string) => {
|
|
this.switchingToChannel.delete(channelId);
|
|
};
|
|
|
|
setEnablingCRT = (value: boolean) => {
|
|
this.enablingCRT = value;
|
|
};
|
|
|
|
isEnablingCRT = () => {
|
|
return this.enablingCRT;
|
|
};
|
|
|
|
private getCanJoinOtherTeamsSubject = (serverUrl: string) => {
|
|
if (!this.canJoinOtherTeams[serverUrl]) {
|
|
this.canJoinOtherTeams[serverUrl] = new BehaviorSubject(false);
|
|
}
|
|
|
|
return this.canJoinOtherTeams[serverUrl];
|
|
};
|
|
|
|
observeCanJoinOtherTeams = (serverUrl: string) => {
|
|
return this.getCanJoinOtherTeamsSubject(serverUrl).asObservable();
|
|
};
|
|
|
|
setCanJoinOtherTeams = (serverUrl: string, value: boolean) => {
|
|
this.getCanJoinOtherTeamsSubject(serverUrl).next(value);
|
|
};
|
|
|
|
setNotificationTapped = (value: boolean) => {
|
|
this.notificationTapped = value;
|
|
};
|
|
|
|
wasNotificationTapped = () => {
|
|
return this.notificationTapped;
|
|
};
|
|
|
|
setAcknowledgingPost = (postId: string) => {
|
|
this.acknowledgingPost.add(postId);
|
|
};
|
|
|
|
unsetAcknowledgingPost = (postId: string) => {
|
|
this.acknowledgingPost.delete(postId);
|
|
};
|
|
|
|
isAcknowledgingPost = (postId: string) => {
|
|
return this.acknowledgingPost.has(postId);
|
|
};
|
|
|
|
setUnacknowledgingPost = (postId: string) => {
|
|
this.unacknowledgingPost.add(postId);
|
|
};
|
|
|
|
unsetUnacknowledgingPost = (postId: string) => {
|
|
this.unacknowledgingPost.delete(postId);
|
|
};
|
|
|
|
isUnacknowledgingPost = (postId: string) => {
|
|
return this.unacknowledgingPost.has(postId);
|
|
};
|
|
|
|
getChannelPlaybooksSynced = (serverUrl: string, channelId: string) => {
|
|
return this.channelPlaybooksSynced[serverUrl]?.has(channelId) ?? false;
|
|
};
|
|
|
|
setChannelPlaybooksSynced = (serverUrl: string, channelId: string) => {
|
|
if (!this.channelPlaybooksSynced[serverUrl]) {
|
|
this.channelPlaybooksSynced[serverUrl] = new Set();
|
|
}
|
|
this.channelPlaybooksSynced[serverUrl]?.add(channelId);
|
|
};
|
|
|
|
unsetChannelPlaybooksSynced = (serverUrl: string, channelId: string) => {
|
|
this.channelPlaybooksSynced[serverUrl]?.delete(channelId);
|
|
};
|
|
|
|
clearChannelPlaybooksSynced = (serverUrl: string) => {
|
|
delete this.channelPlaybooksSynced[serverUrl];
|
|
};
|
|
|
|
observeTheme = () => {
|
|
return this.themeSubject.asObservable();
|
|
};
|
|
|
|
setTheme = (theme: Theme) => {
|
|
this.themeSubject.next(theme);
|
|
};
|
|
|
|
getTheme = () => {
|
|
return this.themeSubject.value;
|
|
};
|
|
|
|
getManagedCategoryPropertyIds = (serverUrl: string) => {
|
|
return this.managedCategoryPropertyIds[serverUrl];
|
|
};
|
|
|
|
setManagedCategoryPropertyIds = (serverUrl: string, ids: {groupId: string; fieldId: string}) => {
|
|
this.managedCategoryPropertyIds[serverUrl] = ids;
|
|
};
|
|
|
|
clearManagedCategoryPropertyIds = (serverUrl: string) => {
|
|
delete this.managedCategoryPropertyIds[serverUrl];
|
|
};
|
|
|
|
// Ephemeral cache for the classification banner fields fetch
|
|
shouldFetchClassificationBanner = (serverUrl: string) => {
|
|
const ts = this.classificationBannerFetchedAt[serverUrl];
|
|
if (ts === undefined) {
|
|
return true;
|
|
}
|
|
return Date.now() - ts >= CLASSIFICATION_BANNER_CACHE_TTL;
|
|
};
|
|
|
|
setClassificationBannerFetched = (serverUrl: string) => {
|
|
this.classificationBannerFetchedAt[serverUrl] = Date.now();
|
|
};
|
|
|
|
getClassificationFieldSyncAttempted = (serverUrl: string, optionId: string) => {
|
|
return this.classificationFieldSyncAttempted[serverUrl]?.has(optionId) ?? false;
|
|
};
|
|
|
|
setClassificationFieldSyncAttempted = (serverUrl: string, optionId: string) => {
|
|
if (!this.classificationFieldSyncAttempted[serverUrl]) {
|
|
this.classificationFieldSyncAttempted[serverUrl] = new Set();
|
|
}
|
|
this.classificationFieldSyncAttempted[serverUrl]?.add(optionId);
|
|
};
|
|
|
|
clearClassificationCache = (serverUrl: string) => {
|
|
delete this.classificationBannerFetchedAt[serverUrl];
|
|
delete this.classificationFieldSyncAttempted[serverUrl];
|
|
};
|
|
|
|
getChannelAttributeValuesSynced = (serverUrl: string, channelId: string) => {
|
|
return this.channelAttributeValuesSynced[serverUrl]?.has(channelId) ?? false;
|
|
};
|
|
|
|
setChannelAttributeValuesSynced = (serverUrl: string, channelId: string) => {
|
|
if (!this.channelAttributeValuesSynced[serverUrl]) {
|
|
this.channelAttributeValuesSynced[serverUrl] = new Set();
|
|
}
|
|
this.channelAttributeValuesSynced[serverUrl]?.add(channelId);
|
|
};
|
|
|
|
unsetChannelAttributeValuesSynced = (serverUrl: string, channelId: string) => {
|
|
this.channelAttributeValuesSynced[serverUrl]?.delete(channelId);
|
|
};
|
|
|
|
clearChannelAttributeValuesSynced = (serverUrl: string) => {
|
|
delete this.channelAttributeValuesSynced[serverUrl];
|
|
};
|
|
|
|
// Ephemeral control for rejected files
|
|
addRejectedFile = (fileId: string, rejectionReason?: string) => {
|
|
this.rejectedFiles.set(fileId, rejectionReason || '');
|
|
|
|
// Emit event so components can re-render with the updated rejection status
|
|
DeviceEventEmitter.emit(Events.FILE_REJECTED, {fileId});
|
|
};
|
|
|
|
isFileRejected = (fileId: string) => {
|
|
return this.rejectedFiles.has(fileId);
|
|
};
|
|
|
|
getRejectionReason = (fileId: string) => {
|
|
return this.rejectedFiles.get(fileId);
|
|
};
|
|
|
|
clearRejectedFiles = () => {
|
|
this.rejectedFiles.clear();
|
|
};
|
|
}
|
|
|
|
const EphemeralStore = new EphemeralStoreSingleton();
|
|
export default EphemeralStore;
|