mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-09 12:40:12 +00:00
Wires the DraftSyncManager's reconcile/WS entry points (previously defined but never called in production) into the app's WebSocket and lifecycle flow. Design choice: WS draft events are treated as CHANGE SIGNALS that trigger an authoritative GET reconcile of the current team, not applied directly — this reuses Phase 4's matrix/scope/absence/observation-fence, and the server's silent empty-upsert delete (no event) forces GET-reconciliation regardless. The current-team GET also returns the user's DM/GM drafts, so it converges them too; other teams' channel drafts converge on team switch. Triggers: - draft_created / draft_deleted WS events -> enqueue -> reconcile current team (constants + app/actions/websocket/draft.ts handler + event dispatch cases). - Reconnect/first-connect: requestReconcile in doReconnect after entry batches. - Team switch: requestReconcile in handleTeamChange (before the tablet early return, so both paths fire). - Foreground: WebsocketManager forwards app-active to DraftSyncManager.onForeground (covers a socket that stayed open, so no reconnect fired). No second AppState listener is added. - Capability change: handleCapabilityChange after config (AllowSyncedDrafts) and after sync_drafts preference create/update/delete batches. Also adds a central empty-team guard in requestReconcile (the GET is per-team). Deferred (noted for a follow-up): Global Drafts stale-on-open reconcile (touches a screen, needs on-device verification); direct-apply of buffered WS events (the buffer is retained, reserved for that future optimization). tsc + eslint clean; new/updated unit tests across manager, ws handler, event dispatch, reconnect, team switch, config/preference capability. All affected suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
5.9 KiB
TypeScript
152 lines
5.9 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {updateDmGmDisplayName} from '@actions/local/channel';
|
|
import {fetchPostById} from '@actions/remote/post';
|
|
import {handleCRTToggled} from '@actions/remote/preference';
|
|
import {Preferences} from '@constants';
|
|
import DatabaseManager from '@database/manager';
|
|
import DraftSyncManager from '@managers/draft_sync_manager';
|
|
import {getPostById} from '@queries/servers/post';
|
|
import {deletePreferences, differsFromLocalNameFormat, getHasCRTChanged} from '@queries/servers/preference';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import {logDebug} from '@utils/log';
|
|
|
|
// affectsDraftSync: whether a preference batch includes the advanced_settings/sync_drafts opt-out that
|
|
// gates draft synchronization, so the coordinator's capability only re-evaluates when it actually changed.
|
|
function affectsDraftSync(preferences: PreferenceType[]) {
|
|
return preferences.some((p) =>
|
|
p.category === Preferences.CATEGORIES.ADVANCED_SETTINGS && p.name === Preferences.ADVANCED_SYNC_DRAFTS);
|
|
}
|
|
|
|
function filterStaleSavedPostPreferences(serverUrl: string, preferences: PreferenceType[]) {
|
|
return preferences.filter((preference) => {
|
|
if (preference.category !== Preferences.CATEGORIES.SAVED_POST || preference.value !== 'true') {
|
|
return true;
|
|
}
|
|
|
|
return !EphemeralStore.isRecentlyUnsavedSavedPost(serverUrl, preference.name);
|
|
});
|
|
}
|
|
|
|
export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
|
if (EphemeralStore.isEnablingCRT()) {
|
|
logDebug('[handlePreferenceChangedEvent] skipping: CRT is being enabled');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const preference: PreferenceType = JSON.parse(msg.data.preference);
|
|
logDebug('[handlePreferenceChangedEvent] PREFERENCE_CHANGED', preference.category, preference.name);
|
|
const preferences = filterStaleSavedPostPreferences(serverUrl, [preference]);
|
|
|
|
// Empty only when the single preference was a stale SAVED_POST re-save — safe to skip.
|
|
if (!preferences.length) {
|
|
return;
|
|
}
|
|
|
|
handleSavePostAdded(serverUrl, preferences);
|
|
|
|
const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, preferences);
|
|
const crtToggled = await getHasCRTChanged(database, preferences);
|
|
|
|
await operator.handlePreferences({
|
|
prepareRecordsOnly: false,
|
|
preferences,
|
|
});
|
|
|
|
if (hasDiffNameFormatPref) {
|
|
updateDmGmDisplayName(serverUrl);
|
|
}
|
|
|
|
if (crtToggled) {
|
|
handleCRTToggled(serverUrl);
|
|
}
|
|
|
|
if (affectsDraftSync(preferences)) {
|
|
await DraftSyncManager.handleCapabilityChange(serverUrl);
|
|
}
|
|
} catch (error) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
|
if (EphemeralStore.isEnablingCRT()) {
|
|
logDebug('[handlePreferencesChangedEvent] skipping: CRT is being enabled');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const preferences: PreferenceType[] = filterStaleSavedPostPreferences(serverUrl, JSON.parse(msg.data.preferences));
|
|
logDebug('[handlePreferencesChangedEvent] PREFERENCES_CHANGED', preferences.map((p) => `${p.category}/${p.name}`).join(', '));
|
|
|
|
// filterStaleSavedPostPreferences only removes SAVED_POST entries with value='true' that
|
|
// are in the recently-unsaved set; all other preference categories pass through unchanged.
|
|
// An empty result here means the entire batch was stale SAVED_POST re-saves — safe to skip.
|
|
if (!preferences.length) {
|
|
return;
|
|
}
|
|
|
|
handleSavePostAdded(serverUrl, preferences);
|
|
|
|
const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, preferences);
|
|
const crtToggled = await getHasCRTChanged(database, preferences);
|
|
|
|
await operator.handlePreferences({
|
|
prepareRecordsOnly: false,
|
|
preferences,
|
|
});
|
|
|
|
if (hasDiffNameFormatPref) {
|
|
updateDmGmDisplayName(serverUrl);
|
|
}
|
|
|
|
if (crtToggled) {
|
|
handleCRTToggled(serverUrl);
|
|
}
|
|
|
|
if (affectsDraftSync(preferences)) {
|
|
await DraftSyncManager.handleCapabilityChange(serverUrl);
|
|
}
|
|
} catch (error) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
export async function handlePreferencesDeletedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
|
try {
|
|
const databaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
|
|
|
|
// Await so a following capability re-read observes the committed deletion.
|
|
await deletePreferences(databaseAndOperator, preferences);
|
|
|
|
// Deleting the sync_drafts opt-out reverts draft sync to its default (enabled): re-evaluate.
|
|
if (affectsDraftSync(preferences)) {
|
|
await DraftSyncManager.handleCapabilityChange(serverUrl);
|
|
}
|
|
} catch {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
// If preferences include new save posts we fetch them
|
|
async function handleSavePostAdded(serverUrl: string, preferences: PreferenceType[]) {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST);
|
|
|
|
for await (const saved of savedPosts) {
|
|
const post = await getPostById(database, saved.name);
|
|
if (!post) {
|
|
await fetchPostById(serverUrl, saved.name, false);
|
|
}
|
|
}
|
|
} catch {
|
|
// Do nothing
|
|
}
|
|
}
|