diff --git a/lib/shared/updates/join-thread-spec.js b/lib/shared/updates/join-thread-spec.js index 22bac66b8..273f4ef24 100644 --- a/lib/shared/updates/join-thread-spec.js +++ b/lib/shared/updates/join-thread-spec.js @@ -1,181 +1,184 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import t from 'tcomb'; import type { UpdateInfoFromRawInfoParams, UpdateSpec } from './update-spec.js'; import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import { type RawEntryInfo, rawEntryInfoValidator, } from '../../types/entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../../types/message-types.js'; import { messageTruncationStatusValidator, rawMessageInfoValidator, } from '../../types/message-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, ThreadJoinUpdateData, } from '../../types/update-types.js'; import { tNumber, tShape } from '../../utils/validation-utils.js'; import { threadInFilterList } from '../thread-utils.js'; import { combineTruncationStatuses } from '../truncation-utils.js'; export const joinThreadSpec: UpdateSpec< ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, ThreadJoinUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadJoinUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } invariant( update.threadInfo.minimallyEncoded, 'update threadInfo must be minimallyEncoded', ); return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, mergeEntryInfos( entryIDs: Set, mergedEntryInfos: Array, update: ThreadJoinUpdateInfo, ) { for (const entryInfo of update.rawEntryInfos) { const entryID = entryInfo.id; if (!entryID || entryIDs.has(entryID)) { continue; } mergedEntryInfos.push(entryInfo); entryIDs.add(entryID); } }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadJoinUpdateInfo, ) { if ( !threadInFilterList(update.threadInfo) || filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set([...filteredThreadIDs, update.threadInfo.id]); }, getRawMessageInfos(update: ThreadJoinUpdateInfo) { return update.rawMessageInfos; }, mergeMessageInfosAndTruncationStatuses( messageIDs: Set, messageInfos: Array, truncationStatuses: MessageTruncationStatuses, update: ThreadJoinUpdateInfo, ) { for (const messageInfo of update.rawMessageInfos) { const messageID = messageInfo.id; if (!messageID || messageIDs.has(messageID)) { continue; } messageInfos.push(messageInfo); messageIDs.add(messageID); } truncationStatuses[update.threadInfo.id] = combineTruncationStatuses( update.truncationStatus, truncationStatuses[update.threadInfo.id], ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.JOIN_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, updateContentForServerDB(data: ThreadJoinUpdateData) { const { threadID } = data; return JSON.stringify({ threadID }); }, entitiesToFetch(update: ThreadJoinRawUpdateInfo) { return { threadID: update.threadID, detailedThreadID: update.threadID, }; }, rawInfoFromData(data: ThreadJoinUpdateData, id: string) { return { type: updateTypes.JOIN_THREAD, id, time: data.time, threadID: data.threadID, }; }, updateInfoFromRawInfo( info: ThreadJoinRawUpdateInfo, params: UpdateInfoFromRawInfoParams, ) { const { data, rawEntryInfosByThreadID, rawMessageInfosByThreadID } = params; const { threadInfos, calendarResult, messageInfosResult } = data; const threadInfo = threadInfos[info.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.JOIN_THREAD because we couldn't " + `fetch RawThreadInfo for ${info.threadID}`, ); return null; } invariant(calendarResult, 'should be set'); const rawEntryInfos = rawEntryInfosByThreadID[info.threadID] ?? []; invariant(messageInfosResult, 'should be set'); const rawMessageInfos = rawMessageInfosByThreadID[info.threadID] ?? []; return { type: updateTypes.JOIN_THREAD, id: info.id, time: info.time, threadInfo, rawMessageInfos, truncationStatus: messageInfosResult.truncationStatuses[info.threadID], rawEntryInfos, }; }, deleteCondition: 'all_types', keyForUpdateData(data: ThreadJoinUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadJoinUpdateInfo) { return info.threadInfo.id; }, typesOfReplacedUpdatesForMatchingKey: 'all_types', infoValidator: tShape({ type: tNumber(updateTypes.JOIN_THREAD), id: t.String, time: t.Number, threadInfo: mixedRawThreadInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatus: messageTruncationStatusValidator, rawEntryInfos: t.list(rawEntryInfoValidator), }), + getUpdatedThreadInfo(update: ThreadJoinUpdateInfo) { + return update.threadInfo; + }, }); diff --git a/lib/shared/updates/update-spec.js b/lib/shared/updates/update-spec.js index fbf5697f1..df63f3820 100644 --- a/lib/shared/updates/update-spec.js +++ b/lib/shared/updates/update-spec.js @@ -1,106 +1,114 @@ // @flow import type { TType } from 'tcomb'; import type { ThreadStoreOperation } from '../../ops/thread-store-ops.js'; import type { UserStoreOperation } from '../../ops/user-store-ops.js'; import type { FetchEntryInfosBase, RawEntryInfo, RawEntryInfos, } from '../../types/entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, FetchMessageInfosResult, } from '../../types/message-types.js'; +import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { RawThreadInfos, MixedRawThreadInfos, + LegacyRawThreadInfo, } from '../../types/thread-types.js'; import type { UpdateType } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo, RawUpdateInfo, UpdateData, } from '../../types/update-types.js'; import type { CurrentUserInfo, LoggedInUserInfo, UserInfos, } from '../../types/user-types.js'; export type UpdateInfosRawData = { +threadInfos: MixedRawThreadInfos, +messageInfosResult: ?FetchMessageInfosResult, +calendarResult: ?FetchEntryInfosBase, +entryInfosResult: ?RawEntryInfos, +currentUserInfoResult: ?LoggedInUserInfo, +userInfosResult: ?UserInfos, }; export type UpdateInfoFromRawInfoParams = { +data: UpdateInfosRawData, +rawEntryInfosByThreadID: { +[id: string]: $ReadOnlyArray, }, +rawMessageInfosByThreadID: { +[id: string]: $ReadOnlyArray, }, }; export type UpdateTypes = 'all_types' | $ReadOnlySet; export type UpdateSpec< UpdateInfo: ClientUpdateInfo, RawInfo: RawUpdateInfo, Data: UpdateData, > = { +generateOpsForThreadUpdates?: ( storeThreadInfos: RawThreadInfos, update: UpdateInfo, ) => ?$ReadOnlyArray, +mergeEntryInfos?: ( entryIDs: Set, mergedEntryInfos: Array, update: UpdateInfo, ) => void, +reduceCurrentUser?: ( state: ?CurrentUserInfo, update: UpdateInfo, ) => ?CurrentUserInfo, +reduceUserInfos?: (state: UserInfos, update: UpdateInfo) => UserInfos, +reduceCalendarThreadFilters?: ( filteredThreadIDs: $ReadOnlySet, update: UpdateInfo, ) => $ReadOnlySet, +getRawMessageInfos?: (update: UpdateInfo) => $ReadOnlyArray, +mergeMessageInfosAndTruncationStatuses?: ( messageIDs: Set, messageInfos: Array, truncationStatuses: MessageTruncationStatuses, update: UpdateInfo, ) => void, +rawUpdateInfoFromRow: (row: Object) => RawInfo, +updateContentForServerDB: (data: Data) => ?string, +entitiesToFetch?: (update: RawInfo) => { +threadID?: string, +detailedThreadID?: string, +entryID?: string, +currentUser?: boolean, +userID?: string, }, +rawInfoFromData: (data: Data, id: string) => RawInfo, +updateInfoFromRawInfo: ( info: RawInfo, params: UpdateInfoFromRawInfoParams, ) => ?UpdateInfo, +deleteCondition: ?UpdateTypes, +keyForUpdateData?: (data: Data) => string, +keyForUpdateInfo?: (info: UpdateInfo) => string, +typesOfReplacedUpdatesForMatchingKey: ?UpdateTypes, +infoValidator: TType, +generateOpsForUserInfoUpdates?: ( update: UpdateInfo, ) => ?$ReadOnlyArray, + +getUpdatedThreadInfo?: ( + update: UpdateInfo, + threadInfos: { + +[string]: LegacyRawThreadInfo | RawThreadInfo, + }, + ) => ?(LegacyRawThreadInfo | RawThreadInfo), }; diff --git a/lib/shared/updates/update-thread-read-status-spec.js b/lib/shared/updates/update-thread-read-status-spec.js index a10f055ee..d4c6fc6f1 100644 --- a/lib/shared/updates/update-thread-read-status-spec.js +++ b/lib/shared/updates/update-thread-read-status-spec.js @@ -1,109 +1,140 @@ // @flow import t from 'tcomb'; import type { UpdateSpec } from './update-spec.js'; -import type { RawThreadInfos } from '../../types/thread-types.js'; +import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import type { + LegacyRawThreadInfo, + RawThreadInfos, +} from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadReadStatusUpdateInfo, ThreadReadStatusRawUpdateInfo, ThreadReadStatusUpdateData, } from '../../types/update-types.js'; import { tID, tNumber, tShape } from '../../utils/validation-utils.js'; export const updateThreadReadStatusSpec: UpdateSpec< ThreadReadStatusUpdateInfo, ThreadReadStatusRawUpdateInfo, ThreadReadStatusUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadReadStatusUpdateInfo, ) { if ( !storeThreadInfos[update.threadID] || storeThreadInfos[update.threadID].currentUser.unread === update.unread ) { return null; } const storeThreadInfo = storeThreadInfos[update.threadID]; let updatedThread; if (storeThreadInfo.minimallyEncoded) { updatedThread = { ...storeThreadInfo, currentUser: { ...storeThreadInfo.currentUser, unread: update.unread, }, }; } else { updatedThread = { ...storeThreadInfo, currentUser: { ...storeThreadInfo.currentUser, unread: update.unread, }, }; } return [ { type: 'replace', payload: { id: update.threadID, threadInfo: updatedThread, }, }, ]; }, rawUpdateInfoFromRow(row: Object) { const { threadID, unread } = JSON.parse(row.content); return { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: row.id.toString(), time: row.time, threadID, unread, }; }, updateContentForServerDB(data: ThreadReadStatusUpdateData) { const { threadID, unread } = data; return JSON.stringify({ threadID, unread }); }, rawInfoFromData(data: ThreadReadStatusUpdateData, id: string) { return { type: updateTypes.UPDATE_THREAD_READ_STATUS, id, time: data.time, threadID: data.threadID, unread: data.unread, }; }, updateInfoFromRawInfo(info: ThreadReadStatusRawUpdateInfo) { return { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: info.id, time: info.time, threadID: info.threadID, unread: info.unread, }; }, deleteCondition: new Set([updateTypes.UPDATE_THREAD_READ_STATUS]), keyForUpdateData(data: ThreadReadStatusUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadReadStatusUpdateInfo) { return info.threadID; }, typesOfReplacedUpdatesForMatchingKey: new Set([ updateTypes.UPDATE_THREAD_READ_STATUS, ]), infoValidator: tShape({ type: tNumber(updateTypes.UPDATE_THREAD_READ_STATUS), id: t.String, time: t.Number, threadID: tID, unread: t.Boolean, }), + getUpdatedThreadInfo( + update: ThreadReadStatusUpdateInfo, + threadInfos: { + +[string]: LegacyRawThreadInfo | RawThreadInfo, + }, + ): ?(LegacyRawThreadInfo | RawThreadInfo) { + const threadInfo = threadInfos[update.threadID]; + if (!threadInfo) { + return null; + } + if (threadInfo.minimallyEncoded) { + return { + ...threadInfo, + currentUser: { + ...threadInfo.currentUser, + unread: update.unread, + }, + }; + } + return { + ...threadInfo, + currentUser: { + ...threadInfo.currentUser, + unread: update.unread, + }, + }; + }, }); diff --git a/lib/shared/updates/update-thread-spec.js b/lib/shared/updates/update-thread-spec.js index 0f40b681c..0389de77d 100644 --- a/lib/shared/updates/update-thread-spec.js +++ b/lib/shared/updates/update-thread-spec.js @@ -1,122 +1,125 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import t from 'tcomb'; import type { UpdateInfoFromRawInfoParams, UpdateSpec } from './update-spec.js'; import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadUpdateInfo, ThreadRawUpdateInfo, ThreadUpdateData, } from '../../types/update-types.js'; import { tNumber, tShape } from '../../utils/validation-utils.js'; import { threadInFilterList } from '../thread-utils.js'; export const updateThreadSpec: UpdateSpec< ThreadUpdateInfo, ThreadRawUpdateInfo, ThreadUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } invariant( update.threadInfo.minimallyEncoded, 'update threadInfo must be minimallyEncoded', ); return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadUpdateInfo, ) { if ( threadInFilterList(update.threadInfo) || !filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set( [...filteredThreadIDs].filter(id => id !== update.threadInfo.id), ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.UPDATE_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, updateContentForServerDB(data: ThreadUpdateData) { return JSON.stringify({ threadID: data.threadID }); }, entitiesToFetch(update: ThreadRawUpdateInfo) { return { threadID: update.threadID, }; }, rawInfoFromData(data: ThreadUpdateData, id: string) { return { type: updateTypes.UPDATE_THREAD, id, time: data.time, threadID: data.threadID, }; }, updateInfoFromRawInfo( info: ThreadRawUpdateInfo, params: UpdateInfoFromRawInfoParams, ) { const threadInfo = params.data.threadInfos[info.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.UPDATE_THREAD because we couldn't " + `fetch RawThreadInfo for ${info.threadID}`, ); return null; } return { type: updateTypes.UPDATE_THREAD, id: info.id, time: info.time, threadInfo, }; }, deleteCondition: new Set([ updateTypes.UPDATE_THREAD, updateTypes.UPDATE_THREAD_READ_STATUS, ]), keyForUpdateData(data: ThreadUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadUpdateInfo) { return info.threadInfo.id; }, typesOfReplacedUpdatesForMatchingKey: new Set([ updateTypes.UPDATE_THREAD_READ_STATUS, ]), infoValidator: tShape({ type: tNumber(updateTypes.UPDATE_THREAD), id: t.String, time: t.Number, threadInfo: mixedRawThreadInfoValidator, }), + getUpdatedThreadInfo(update: ThreadUpdateInfo) { + return update.threadInfo; + }, });