Fix missing all chat folder on startup without new manifest
This commit is contained in:
		
					parent
					
						
							
								bf217a8513
							
						
					
				
			
			
				commit
				
					
						4973b9b204
					
				
			
		
					 9 changed files with 192 additions and 92 deletions
				
			
		|  | @ -1014,6 +1014,17 @@ export async function startApp(): Promise<void> { | |||
|       ) { | ||||
|         await window.storage.put('needProfileMovedModal', true); | ||||
|       } | ||||
| 
 | ||||
|       if (window.isBeforeVersion(lastVersion, 'v7.75.0-beta.1')) { | ||||
|         const hasAllChatsChatFolder = await DataReader.hasAllChatsChatFolder(); | ||||
|         if (!hasAllChatsChatFolder) { | ||||
|           log.info('Creating "all chats" chat folder'); | ||||
|           await DataWriter.createAllChatsChatFolder(); | ||||
|           StorageService.storageServiceUploadJobAfterEnabled({ | ||||
|             reason: 'createAllChatsChatFolder', | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     setAppLoadingScreenMessage( | ||||
|  |  | |||
|  | @ -88,11 +88,7 @@ import { isDone as isRegistrationDone } from '../util/registration.js'; | |||
| import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue.js'; | ||||
| import { isMockEnvironment } from '../environment.js'; | ||||
| import { validateConversation } from '../util/validateConversation.js'; | ||||
| import { | ||||
|   ChatFolderType, | ||||
|   toCurrentChatFolders, | ||||
|   type ChatFolder, | ||||
| } from '../types/ChatFolder.js'; | ||||
| import { hasAllChatsChatFolder, type ChatFolder } from '../types/ChatFolder.js'; | ||||
| 
 | ||||
| const { debounce, isNumber, chunk } = lodash; | ||||
| 
 | ||||
|  | @ -1663,20 +1659,9 @@ async function processManifest( | |||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     const chatFoldersHasAllChatsFolder = chatFolders.some(chatFolder => { | ||||
|       return ( | ||||
|         chatFolder.folderType === ChatFolderType.ALL && | ||||
|         chatFolder.deletedAtTimestampMs === 0 | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     if (!chatFoldersHasAllChatsFolder) { | ||||
|     if (!hasAllChatsChatFolder(chatFolders)) { | ||||
|       log.info(`process(${version}): creating all chats chat folder`); | ||||
|       await DataWriter.createAllChatsChatFolder(); | ||||
|       const currentChatFolders = await DataReader.getCurrentChatFolders(); | ||||
|       window.reduxActions.chatFolders.replaceAllChatFolderRecords( | ||||
|         toCurrentChatFolders(currentChatFolders) | ||||
|       ); | ||||
|       window.reduxActions.chatFolders.createAllChatsChatFolder(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -2235,6 +2220,7 @@ async function upload({ | |||
| } | ||||
| 
 | ||||
| let storageServiceEnabled = false; | ||||
| let storageServiceNeedsUploadAfterEnabled = false; | ||||
| 
 | ||||
| export function enableStorageService(): void { | ||||
|   if (storageServiceEnabled) { | ||||
|  | @ -2243,6 +2229,12 @@ export function enableStorageService(): void { | |||
| 
 | ||||
|   storageServiceEnabled = true; | ||||
|   log.info('enableStorageService'); | ||||
| 
 | ||||
|   if (storageServiceNeedsUploadAfterEnabled) { | ||||
|     storageServiceUploadJob({ | ||||
|       reason: 'storageServiceNeedsUploadAfterEnabled', | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function disableStorageService(reason: string): void { | ||||
|  | @ -2351,6 +2343,18 @@ export async function reprocessUnknownFields(): Promise<void> { | |||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function storageServiceUploadJobAfterEnabled({ | ||||
|   reason, | ||||
| }: { | ||||
|   reason: string; | ||||
| }): void { | ||||
|   if (storageServiceEnabled) { | ||||
|     return storageServiceUploadJob({ reason }); | ||||
|   } | ||||
|   log.info(`storageServiceNeedsUploadAfterEnabled: ${reason}`); | ||||
|   storageServiceNeedsUploadAfterEnabled = true; | ||||
| } | ||||
| 
 | ||||
| export const storageServiceUploadJob = debounce( | ||||
|   ({ reason }: { reason: string }) => { | ||||
|     if (!storageServiceEnabled) { | ||||
|  |  | |||
|  | @ -923,6 +923,7 @@ type ReadableInterface = { | |||
|   getAllChatFolders: () => ReadonlyArray<ChatFolder>; | ||||
|   getCurrentChatFolders: () => ReadonlyArray<ChatFolder>; | ||||
|   getChatFolder: (id: ChatFolderId) => ChatFolder | null; | ||||
|   hasAllChatsChatFolder: () => boolean; | ||||
|   getOldestDeletedChatFolder: () => ChatFolder | null; | ||||
| 
 | ||||
|   getMessagesNeedingUpgrade: ( | ||||
|  |  | |||
|  | @ -240,6 +240,7 @@ import { | |||
|   getCurrentChatFolders, | ||||
|   getChatFolder, | ||||
|   createChatFolder, | ||||
|   hasAllChatsChatFolder, | ||||
|   createAllChatsChatFolder, | ||||
|   updateChatFolder, | ||||
|   markChatFolderDeleted, | ||||
|  | @ -459,6 +460,7 @@ export const DataReader: ServerReadableInterface = { | |||
|   getAllChatFolders, | ||||
|   getCurrentChatFolders, | ||||
|   getChatFolder, | ||||
|   hasAllChatsChatFolder, | ||||
|   getOldestDeletedChatFolder, | ||||
| 
 | ||||
|   callLinkExists, | ||||
|  |  | |||
|  | @ -146,6 +146,19 @@ export function createChatFolder(db: WritableDB, chatFolder: ChatFolder): void { | |||
|   })(); | ||||
| } | ||||
| 
 | ||||
| export function hasAllChatsChatFolder(db: ReadableDB): boolean { | ||||
|   const [query, params] = sql` | ||||
|     SELECT EXISTS ( | ||||
|       SELECT 1 FROM chatFolders | ||||
|       WHERE folderType IS ${ChatFolderType.ALL} | ||||
|       AND deletedAtTimestampMs IS 0 | ||||
|       LIMIT 1 | ||||
|     ) | ||||
|   `;
 | ||||
|   const result = db.prepare(query, { pluck: true }).get<number>(params); | ||||
|   return result === 1; | ||||
| } | ||||
| 
 | ||||
| export function createAllChatsChatFolder(db: WritableDB): ChatFolder { | ||||
|   return db.transaction(() => { | ||||
|     const allChatsChatFolder: ChatFolder = { | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ import { | |||
|   type CurrentChatFolders, | ||||
| } from '../../types/ChatFolder.js'; | ||||
| import { getCurrentChatFolders } from '../selectors/chatFolders.js'; | ||||
| import { DataWriter } from '../../sql/Client.js'; | ||||
| import { DataReader, DataWriter } from '../../sql/Client.js'; | ||||
| import { storageServiceUploadJob } from '../../services/storage.js'; | ||||
| import { parseStrict } from '../../util/schemas.js'; | ||||
| import { chatFolderCleanupService } from '../../services/expiring/chatFolderCleanupService.js'; | ||||
|  | @ -142,6 +142,20 @@ function createChatFolder( | |||
|   }; | ||||
| } | ||||
| 
 | ||||
| function createAllChatsChatFolder(): ThunkAction< | ||||
|   void, | ||||
|   RootStateType, | ||||
|   unknown, | ||||
|   ChatFolderRecordReplaceAll | ||||
| > { | ||||
|   return async dispatch => { | ||||
|     await DataWriter.createAllChatsChatFolder(); | ||||
|     storageServiceUploadJob({ reason: 'createAllChatsChatFolder' }); | ||||
|     const chatFolders = await DataReader.getCurrentChatFolders(); | ||||
|     dispatch(replaceAllChatFolderRecords(toCurrentChatFolders(chatFolders))); | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function updateChatFolder( | ||||
|   chatFolderId: ChatFolderId, | ||||
|   chatFolderParams: ChatFolderParams | ||||
|  | @ -210,6 +224,7 @@ export const actions = { | |||
|   replaceChatFolderRecord, | ||||
|   removeChatFolderRecord, | ||||
|   createChatFolder, | ||||
|   createAllChatsChatFolder, | ||||
|   updateChatFolder, | ||||
|   deleteChatFolder, | ||||
|   updateChatFoldersPositions, | ||||
|  |  | |||
|  | @ -3,10 +3,11 @@ | |||
| import Long from 'long'; | ||||
| import { v4 as generateUuid } from 'uuid'; | ||||
| import { Proto, StorageState } from '@signalapp/mock-server'; | ||||
| import type { Page } from 'playwright/test'; | ||||
| import { expect } from 'playwright/test'; | ||||
| import * as durations from '../../util/durations/index.js'; | ||||
| import type { App } from './fixtures.js'; | ||||
| import { Bootstrap, debug } from './fixtures.js'; | ||||
| import { Bootstrap, debug, getChatFolderRecordPredicate } from './fixtures.js'; | ||||
| import { uuidToBytes } from '../../util/uuidToBytes.js'; | ||||
| import { CHAT_FOLDER_DELETED_POSITION } from '../../types/ChatFolder.js'; | ||||
| 
 | ||||
|  | @ -48,10 +49,7 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|     await bootstrap.teardown(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should update from storage service', async () => { | ||||
|     const { phone } = bootstrap; | ||||
|     const window = await app.getWindow(); | ||||
| 
 | ||||
|   async function openChatFolderSettings(window: Page) { | ||||
|     const openSettingsBtn = window.locator( | ||||
|       '[data-testid="NavTabsItem--Settings"]' | ||||
|     ); | ||||
|  | @ -60,50 +58,42 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       '.Preferences__control:has-text("Add a chat folder")' | ||||
|     ); | ||||
| 
 | ||||
|     const ALL_CHATS_ID = generateUuid(); | ||||
|     await openSettingsBtn.click(); | ||||
|     await openChatsSettingsBtn.click(); | ||||
|     await openChatFoldersSettingsBtn.click(); | ||||
|   } | ||||
| 
 | ||||
|   it('should update from storage service', async () => { | ||||
|     const { phone } = bootstrap; | ||||
|     const window = await app.getWindow(); | ||||
| 
 | ||||
|     const ALL_CHATS_PREDICATE = getChatFolderRecordPredicate('ALL', '', false); | ||||
| 
 | ||||
|     const ALL_GROUPS_ID = generateUuid(); | ||||
|     const ALL_GROUPS_NAME = 'All Groups'; | ||||
|     const ALL_GROUPS_NAME_UPDATED = 'The Groups'; | ||||
| 
 | ||||
|     const allChatsListItem = window.getByTestId(`ChatFolder--${ALL_CHATS_ID}`); | ||||
|     const allChatsListItem = window | ||||
|       .getByTestId('ChatFoldersList') | ||||
|       .locator('.Preferences__ChatFolders__ChatSelection__Item') | ||||
|       .getByText('All chats'); | ||||
| 
 | ||||
|     const allGroupsListItem = window.getByTestId( | ||||
|       `ChatFolder--${ALL_GROUPS_ID}` | ||||
|     ); | ||||
| 
 | ||||
|     await openSettingsBtn.click(); | ||||
|     await openChatsSettingsBtn.click(); | ||||
|     await openChatFoldersSettingsBtn.click(); | ||||
| 
 | ||||
|     debug('adding ALL chat folder via storage service'); | ||||
|     { | ||||
|       let state = await phone.expectStorageState('initial state'); | ||||
| 
 | ||||
|       state = state.addRecord({ | ||||
|         type: IdentifierType.CHAT_FOLDER, | ||||
|         record: { | ||||
|           chatFolder: { | ||||
|             id: uuidToBytes(ALL_CHATS_ID), | ||||
|             folderType: Proto.ChatFolderRecord.FolderType.ALL, | ||||
|             name: null, | ||||
|             position: 0, | ||||
|             showOnlyUnread: false, | ||||
|             showMutedChats: false, | ||||
|             includeAllIndividualChats: true, | ||||
|             includeAllGroupChats: true, | ||||
|             includedRecipients: [], | ||||
|             excludedRecipients: [], | ||||
|             deletedAtTimestampMs: Long.fromNumber(0), | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       await phone.setStorageState(state); | ||||
|       await phone.sendFetchStorage({ timestamp: bootstrap.getTimestamp() }); | ||||
|       await app.waitForManifestVersion(state.version); | ||||
| 
 | ||||
|       await expect(allChatsListItem).toBeVisible(); | ||||
|       // wait for initial creation of story distribution list and "all chats" chat folder
 | ||||
|       state = await phone.waitForStorageState({ after: state }); | ||||
|       expect(state.hasRecord(ALL_CHATS_PREDICATE)).toBe(true); | ||||
|     } | ||||
| 
 | ||||
|     await openChatFolderSettings(window); | ||||
| 
 | ||||
|     debug('expect all chats folder to be created'); | ||||
|     await expect(allChatsListItem).toBeVisible(); | ||||
| 
 | ||||
|     debug('adding "All Groups" chat folder via storage service'); | ||||
|     { | ||||
|       let state = await phone.expectStorageState('adding all groups'); | ||||
|  | @ -140,9 +130,7 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       let state = await phone.expectStorageState('updating all groups'); | ||||
| 
 | ||||
|       state = state.updateRecord( | ||||
|         item => { | ||||
|           return item.record.chatFolder?.name === ALL_GROUPS_NAME; | ||||
|         }, | ||||
|         getChatFolderRecordPredicate('CUSTOM', ALL_GROUPS_NAME, false), | ||||
|         item => { | ||||
|           return { | ||||
|             ...item, | ||||
|  | @ -168,9 +156,7 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       let state = await phone.expectStorageState('removing all groups'); | ||||
| 
 | ||||
|       state = state.updateRecord( | ||||
|         item => { | ||||
|           return item.record.chatFolder?.name === ALL_GROUPS_NAME_UPDATED; | ||||
|         }, | ||||
|         getChatFolderRecordPredicate('CUSTOM', ALL_GROUPS_NAME_UPDATED, false), | ||||
|         item => { | ||||
|           return { | ||||
|             ...item, | ||||
|  | @ -196,13 +182,12 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|     const { phone } = bootstrap; | ||||
|     const window = await app.getWindow(); | ||||
| 
 | ||||
|     const openSettingsBtn = window.locator( | ||||
|       '[data-testid="NavTabsItem--Settings"]' | ||||
|     ); | ||||
|     const openChatsSettingsBtn = window.locator('.Preferences__button--chats'); | ||||
|     const openChatFoldersSettingsBtn = window.locator( | ||||
|       '.Preferences__control:has-text("Add a chat folder")' | ||||
|     ); | ||||
|     const ALL_CHATS_PREDICATE = getChatFolderRecordPredicate('ALL', '', false); | ||||
| 
 | ||||
|     const allChatsListItem = window | ||||
|       .getByTestId('ChatFoldersList') | ||||
|       .locator('.Preferences__ChatFolders__ChatSelection__Item') | ||||
|       .getByText('All chats'); | ||||
| 
 | ||||
|     const groupPresetBtn = window | ||||
|       .getByTestId('ChatFolderPreset--GroupChats') | ||||
|  | @ -228,27 +213,26 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       .locator('button:has-text("Delete")'); | ||||
| 
 | ||||
|     let state = await phone.expectStorageState('initial state'); | ||||
|     // wait for initial creation of story distribution list
 | ||||
|     // wait for initial creation of story distribution list and "all chats" chat folder
 | ||||
|     state = await phone.waitForStorageState({ after: state }); | ||||
|     expect(state.hasRecord(ALL_CHATS_PREDICATE)).toBe(true); | ||||
| 
 | ||||
|     await openChatFolderSettings(window); | ||||
| 
 | ||||
|     debug('expect all chats folder to be created'); | ||||
|     await expect(allChatsListItem).toBeVisible(); | ||||
| 
 | ||||
|     debug('creating group'); | ||||
|     { | ||||
|       await openSettingsBtn.click(); | ||||
|       await openChatsSettingsBtn.click(); | ||||
|       await openChatFoldersSettingsBtn.click(); | ||||
| 
 | ||||
|       await groupPresetBtn.click(); | ||||
|       await expect(groupsFolderBtn).toBeVisible(); | ||||
| 
 | ||||
|       debug('waiting for storage sync'); | ||||
|       state = await phone.waitForStorageState({ after: state }); | ||||
| 
 | ||||
|       const found = state.hasRecord(item => { | ||||
|         return ( | ||||
|           item.type === IdentifierType.CHAT_FOLDER && | ||||
|           item.record.chatFolder?.name === 'Groups' | ||||
|         ); | ||||
|       }); | ||||
|       const found = state.hasRecord( | ||||
|         getChatFolderRecordPredicate('CUSTOM', 'Groups', false) | ||||
|       ); | ||||
| 
 | ||||
|       expect(found).toBe(true); | ||||
|     } | ||||
|  | @ -262,12 +246,9 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       debug('waiting for storage sync'); | ||||
|       state = await phone.waitForStorageState({ after: state }); | ||||
| 
 | ||||
|       const found = state.hasRecord(item => { | ||||
|         return ( | ||||
|           item.type === IdentifierType.CHAT_FOLDER && | ||||
|           item.record.chatFolder?.name === 'My Groups' | ||||
|         ); | ||||
|       }); | ||||
|       const found = state.hasRecord( | ||||
|         getChatFolderRecordPredicate('CUSTOM', 'My Groups', false) | ||||
|       ); | ||||
| 
 | ||||
|       expect(found).toBe(true); | ||||
|     } | ||||
|  | @ -281,12 +262,9 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       debug('waiting for storage sync'); | ||||
|       state = await phone.waitForStorageState({ after: state }); | ||||
| 
 | ||||
|       const found = state.findRecord(item => { | ||||
|         return ( | ||||
|           item.type === IdentifierType.CHAT_FOLDER && | ||||
|           item.record.chatFolder?.name === 'My Groups' | ||||
|         ); | ||||
|       }); | ||||
|       const found = state.findRecord( | ||||
|         getChatFolderRecordPredicate('CUSTOM', 'My Groups', true) | ||||
|       ); | ||||
| 
 | ||||
|       await expect(groupsFolderBtn).not.toBeAttached(); | ||||
|       await expect(groupPresetBtn).toBeVisible(); | ||||
|  | @ -296,4 +274,46 @@ describe('storage service/chat folders', function (this: Mocha.Suite) { | |||
|       ).toBeGreaterThan(0); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   it('should recover from all chats folder being deleted', async () => { | ||||
|     const { phone } = bootstrap; | ||||
|     const window = await app.getWindow(); | ||||
| 
 | ||||
|     const ALL_CHATS_PREDICATE = getChatFolderRecordPredicate('ALL', '', false); | ||||
| 
 | ||||
|     let state = await phone.expectStorageState('initial state'); | ||||
|     expect(state.version).toBe(1); | ||||
|     expect(state.hasRecord(ALL_CHATS_PREDICATE)).toBe(false); | ||||
| 
 | ||||
|     // wait for initial creation of story distribution list and "all chats" chat folder
 | ||||
|     state = await phone.waitForStorageState({ after: state }); | ||||
|     expect(state.version).toBe(2); | ||||
|     expect(state.hasRecord(ALL_CHATS_PREDICATE)).toBe(true); | ||||
| 
 | ||||
|     await openChatFolderSettings(window); | ||||
| 
 | ||||
|     // update record
 | ||||
|     state = state.updateRecord(ALL_CHATS_PREDICATE, item => { | ||||
|       return { | ||||
|         ...item, | ||||
|         chatFolder: { | ||||
|           ...item.chatFolder, | ||||
|           position: CHAT_FOLDER_DELETED_POSITION, | ||||
|           deletedAtTimestampMs: Long.fromNumber(Date.now()), | ||||
|         }, | ||||
|       }; | ||||
|     }); | ||||
|     state = await phone.setStorageState(state); | ||||
|     expect(state.version).toBe(3); | ||||
|     expect(state.hasRecord(ALL_CHATS_PREDICATE)).toBe(false); | ||||
| 
 | ||||
|     // sync from phone to app
 | ||||
|     await phone.sendFetchStorage({ timestamp: bootstrap.getTimestamp() }); | ||||
|     await app.waitForManifestVersion(state.version); | ||||
| 
 | ||||
|     // wait for app to insert a new "All chats" chat folder
 | ||||
|     state = await phone.waitForStorageState({ after: state }); | ||||
|     expect(state.version).toBe(4); | ||||
|     expect(state.hasRecord(ALL_CHATS_PREDICATE)).toBe(true); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -239,3 +239,26 @@ export function getCallLinkRecordPredicate( | |||
|     return roomId === recordRoomId; | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function getChatFolderRecordPredicate( | ||||
|   folderType: keyof typeof Proto.ChatFolderRecord.FolderType, | ||||
|   name: string, | ||||
|   deleted: boolean | ||||
| ): (record: StorageStateRecord) => boolean { | ||||
|   return ({ type, record }) => { | ||||
|     const { chatFolder } = record; | ||||
|     if (type !== IdentifierType.CHAT_FOLDER || chatFolder == null) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     const deletedAtTimestampMs = | ||||
|       chatFolder.deletedAtTimestampMs?.toNumber() ?? 0; | ||||
|     const isDeleted = deletedAtTimestampMs > 0; | ||||
| 
 | ||||
|     return ( | ||||
|       chatFolder.folderType === Proto.ChatFolderRecord.FolderType[folderType] && | ||||
|       chatFolder.name === name && | ||||
|       isDeleted === deleted | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -271,3 +271,14 @@ export function lookupCurrentChatFolder( | |||
|   strictAssert(chatFolder != null, 'Missing chat folder'); | ||||
|   return chatFolder; | ||||
| } | ||||
| 
 | ||||
| export function hasAllChatsChatFolder( | ||||
|   chatFolders: ReadonlyArray<ChatFolder> | ||||
| ): boolean { | ||||
|   return chatFolders.some(chatFolder => { | ||||
|     return ( | ||||
|       chatFolder.folderType === ChatFolderType.ALL && | ||||
|       chatFolder.deletedAtTimestampMs === 0 | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jamie Kyle
				Jamie Kyle