diff --git a/server/src/fetchers/message-fetchers.js b/server/src/fetchers/message-fetchers.js --- a/server/src/fetchers/message-fetchers.js +++ b/server/src/fetchers/message-fetchers.js @@ -31,6 +31,7 @@ mergeOrConditions, mergeAndConditions, } from '../database/database'; +import type { SQLStatementType } from '../database/types'; import type { PushInfo } from '../push/send'; import type { Viewer } from '../session/viewer'; import { creationString, localIDFromCreationString } from '../utils/idempotent'; @@ -255,7 +256,10 @@ criteria: MessageSelectionCriteria, numberPerThread: number, ): Promise<FetchMessageInfosResult> { - const selectionClause = messageSelectionCriteriaToSQLClause(viewer, criteria); + const { + sqlClause: selectionClause, + timeFilterData, + } = parseMessageSelectionCriteria(viewer, criteria); const truncationStatuses = {}; const viewerID = viewer.id; @@ -326,11 +330,7 @@ } continue; } - const hasTimeFilter = messageSelectionCriteriaHasTimeFilterForThread( - viewer, - criteria, - threadID, - ); + const hasTimeFilter = hasTimeFilterForThread(timeFilterData, threadID); if (!hasTimeFilter) { // If there is no time filter for a given thread, and there are fewer // messages returned than the max we queried for a given thread, we can @@ -351,11 +351,7 @@ if (truncationStatus !== null && truncationStatus !== undefined) { continue; } - const hasTimeFilter = messageSelectionCriteriaHasTimeFilterForThread( - viewer, - criteria, - threadID, - ); + const hasTimeFilter = hasTimeFilterForThread(timeFilterData, threadID); if (!hasTimeFilter) { // If there is no time filter for a given thread, and zero messages were // returned, we can conclude that this thread has zero messages. This is @@ -381,49 +377,38 @@ }; } -// This function is set up to track the behavior of -// messageSelectionCriteriaToSQLClause, and any changes there must be kept in -// sync here. -function messageSelectionCriteriaHasTimeFilterForThread( - viewer: Viewer, - criteria: MessageSelectionCriteria, +function hasTimeFilterForThread( + timeFilterData: TimeFilterData, threadID: string, ) { - // If newerThan is set, then a global time filter is applied, no matter what - if (criteria.newerThan) { + if (timeFilterData.timeFilter === 'ALL') { return true; - } - - // If newerThan isn't set, and threadID is present in threadCursors, then - // there definitely is no time filter for this thread, since: - // (1) There is no global time filter because newerThan isn't set and - // threadCursors is - // (2) The threadCursors clause will not be time-filtered, even if - // joinedThreads is (and those clauses are OR'd) - if (criteria.threadCursors && threadID in criteria.threadCursors) { - return false; - } - - // If the above two conditions don't match, then we have to ask if there is a - // default global time filter applied. If we're not applying a default global - // time filter, then there's no time filter here - const shouldApplyTimeFilter = hasMinCodeVersion(viewer.platformDetails, 130); - if (!shouldApplyTimeFilter) { + } else if (timeFilterData.timeFilter === 'NONE') { return false; + } else if (timeFilterData.timeFilter === 'ALL_EXCEPT_EXCLUDED') { + return !timeFilterData.excludedFromTimeFilter.has(threadID); + } else { + invariant( + false, + `unrecognized timeFilter type ${timeFilterData.timeFilter}`, + ); } - - // If threadCursors is set, we never apply a global time filter - return !criteria.threadCursors; } -// The function defined above (messageSelectionCriteriaHasTimeFilterForThread) -// is set up to track the behavior of this function -// (messageSelectionCriteriaToSQLClause), and any changes here must be kept in -// sync there. -function messageSelectionCriteriaToSQLClause( +type TimeFilterData = + | { +timeFilter: 'ALL' | 'NONE' } + | { + +timeFilter: 'ALL_EXCEPT_EXCLUDED', + +excludedFromTimeFilter: $ReadOnlySet<string>, + }; +type ParsedMessageSelectionCriteria = { + +sqlClause: SQLStatementType, + +timeFilterData: TimeFilterData, +}; +function parseMessageSelectionCriteria( viewer: Viewer, criteria: MessageSelectionCriteria, -) { +): ParsedMessageSelectionCriteria { const minMessageTime = Date.now() - defaultMaxMessageAge; const shouldApplyTimeFilter = hasMinCodeVersion(viewer.platformDetails, 130); @@ -461,8 +446,27 @@ } const threadClause = mergeOrConditions(threadConditions); + let timeFilterData; + if (globalTimeFilter) { + timeFilterData = { timeFilter: 'ALL' }; + } else if (!shouldApplyTimeFilter) { + timeFilterData = { timeFilter: 'NONE' }; + } else { + invariant( + criteria.threadCursors, + 'ALL_EXCEPT_EXCLUDED should correspond to threadCursors being set', + ); + const excludedFromTimeFilter = new Set(Object.keys(criteria.threadCursors)); + timeFilterData = { + timeFilter: 'ALL_EXCEPT_EXCLUDED', + excludedFromTimeFilter, + }; + } + const conditions = [globalTimeFilter, threadClause].filter(Boolean); - return mergeAndConditions(conditions); + const sqlClause = mergeAndConditions(conditions); + + return { sqlClause, timeFilterData }; } function messageSelectionCriteriaToInitialTruncationStatuses( @@ -483,7 +487,10 @@ criteria: MessageSelectionCriteria, maxNumberPerThread: number, ): Promise<FetchMessageInfosResult> { - const selectionClause = messageSelectionCriteriaToSQLClause(viewer, criteria); + const { sqlClause: selectionClause } = parseMessageSelectionCriteria( + viewer, + criteria, + ); const truncationStatuses = messageSelectionCriteriaToInitialTruncationStatuses( criteria, messageTruncationStatus.UNCHANGED,