|  |  |  | @ -7,11 +7,13 @@ import { ConversationController } from '../../session/conversations'; | 
		
	
		
			
				|  |  |  |  | import { UserUtils } from '../../session/utils'; | 
		
	
		
			
				|  |  |  |  | import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; | 
		
	
		
			
				|  |  |  |  | import { DAYS, MINUTES } from '../../session/utils/Number'; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | import { | 
		
	
		
			
				|  |  |  |  |   createOrUpdateItem, | 
		
	
		
			
				|  |  |  |  |   generateAttachmentKeyIfEmpty, | 
		
	
		
			
				|  |  |  |  |   getItemById, | 
		
	
		
			
				|  |  |  |  |   hasSyncedInitialConfigurationItem, | 
		
	
		
			
				|  |  |  |  |   removeItemById, | 
		
	
		
			
				|  |  |  |  |   lastAvatarUploadTimestamp, | 
		
	
		
			
				|  |  |  |  | } from '../../data/data'; | 
		
	
		
			
				|  |  |  |  | import { OnionPaths } from '../../session/onions'; | 
		
	
		
			
				|  |  |  |  | import { getMessageQueue } from '../../session/sending'; | 
		
	
	
		
			
				
					|  |  |  | @ -29,11 +31,18 @@ import { useInterval } from '../../hooks/useInterval'; | 
		
	
		
			
				|  |  |  |  | import { clearSearch } from '../../state/ducks/search'; | 
		
	
		
			
				|  |  |  |  | import { showLeftPaneSection } from '../../state/ducks/section'; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager'; | 
		
	
		
			
				|  |  |  |  | import { | 
		
	
		
			
				|  |  |  |  |   cleanUpOldDecryptedMedias, | 
		
	
		
			
				|  |  |  |  |   getDecryptedMediaUrl, | 
		
	
		
			
				|  |  |  |  | } from '../../session/crypto/DecryptedAttachmentsManager'; | 
		
	
		
			
				|  |  |  |  | import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; | 
		
	
		
			
				|  |  |  |  | import { loadDefaultRooms } from '../../opengroup/opengroupV2/ApiUtil'; | 
		
	
		
			
				|  |  |  |  | import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; | 
		
	
		
			
				|  |  |  |  | import { SwarmPolling } from '../../session/snode_api/swarmPolling'; | 
		
	
		
			
				|  |  |  |  | import { IMAGE_JPEG } from '../../types/MIME'; | 
		
	
		
			
				|  |  |  |  | import { FSv2 } from '../../fileserver'; | 
		
	
		
			
				|  |  |  |  | import { stringToArrayBuffer } from '../../session/utils/String'; | 
		
	
		
			
				|  |  |  |  | import { debounce } from 'underscore'; | 
		
	
		
			
				|  |  |  |  | // tslint:disable-next-line: no-import-side-effect no-submodule-imports
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | export enum SectionType { | 
		
	
	
		
			
				
					|  |  |  | @ -45,6 +54,20 @@ export enum SectionType { | 
		
	
		
			
				|  |  |  |  |   Moon, | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | const showUnstableAttachmentsDialogIfNeeded = async () => { | 
		
	
		
			
				|  |  |  |  |   const alreadyShown = (await getItemById('showUnstableAttachmentsDialog'))?.value; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   if (!alreadyShown) { | 
		
	
		
			
				|  |  |  |  |     window.confirmationDialog({ | 
		
	
		
			
				|  |  |  |  |       title: 'File server update', | 
		
	
		
			
				|  |  |  |  |       message: | 
		
	
		
			
				|  |  |  |  |         "We're upgrading the way files are stored. File transfer may be unstable for the next 24-48 hours.", | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     await createOrUpdateItem({ id: 'showUnstableAttachmentsDialog', value: true }); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | }; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | const Section = (props: { type: SectionType; avatarPath?: string }) => { | 
		
	
		
			
				|  |  |  |  |   const ourNumber = useSelector(getOurNumber); | 
		
	
		
			
				|  |  |  |  |   const unreadMessageCount = useSelector(getUnreadMessageCount); | 
		
	
	
		
			
				
					|  |  |  | @ -149,6 +172,81 @@ const triggerSyncIfNeeded = async () => { | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | }; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | const triggerAvatarReUploadIfNeeded = async () => { | 
		
	
		
			
				|  |  |  |  |   const lastTimeStampAvatarUpload = (await getItemById(lastAvatarUploadTimestamp))?.value || 0; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   if (Date.now() - lastTimeStampAvatarUpload > DAYS * 14) { | 
		
	
		
			
				|  |  |  |  |     window.log.info('Reuploading avatar...'); | 
		
	
		
			
				|  |  |  |  |     // reupload the avatar
 | 
		
	
		
			
				|  |  |  |  |     const ourConvo = ConversationController.getInstance().get(UserUtils.getOurPubKeyStrFromCache()); | 
		
	
		
			
				|  |  |  |  |     if (!ourConvo) { | 
		
	
		
			
				|  |  |  |  |       window.log.warn('ourConvo not found... This is not a valid case'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     const profileKey = window.textsecure.storage.get('profileKey'); | 
		
	
		
			
				|  |  |  |  |     if (!profileKey) { | 
		
	
		
			
				|  |  |  |  |       window.log.warn('our profileKey not found... This is not a valid case'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     const currentAttachmentPath = ourConvo.getAvatarPath(); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     if (!currentAttachmentPath) { | 
		
	
		
			
				|  |  |  |  |       window.log.warn('No attachment currently set for our convo.. Nothing to do.'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     if (!decryptedAvatarUrl) { | 
		
	
		
			
				|  |  |  |  |       window.log.warn('Could not decrypt avatar stored locally..'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     const response = await fetch(decryptedAvatarUrl); | 
		
	
		
			
				|  |  |  |  |     const blob = await response.blob(); | 
		
	
		
			
				|  |  |  |  |     const decryptedAvatarData = await blob.arrayBuffer(); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     if (!decryptedAvatarData?.byteLength) { | 
		
	
		
			
				|  |  |  |  |       window.log.warn('Could not read blob of avatar locally..'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     const encryptedData = await window.textsecure.crypto.encryptProfile( | 
		
	
		
			
				|  |  |  |  |       decryptedAvatarData, | 
		
	
		
			
				|  |  |  |  |       profileKey | 
		
	
		
			
				|  |  |  |  |     ); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     const avatarPointer = await FSv2.uploadFileToFsV2(encryptedData); | 
		
	
		
			
				|  |  |  |  |     let fileUrl; | 
		
	
		
			
				|  |  |  |  |     if (!avatarPointer) { | 
		
	
		
			
				|  |  |  |  |       window.log.warn('failed to reupload avatar to fsv2'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     ({ fileUrl } = avatarPointer); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     ourConvo.set('avatarPointer', fileUrl); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     // this encrypts and save the new avatar and returns a new attachment path
 | 
		
	
		
			
				|  |  |  |  |     const upgraded = await window.Signal.Migrations.processNewAttachment({ | 
		
	
		
			
				|  |  |  |  |       isRaw: true, | 
		
	
		
			
				|  |  |  |  |       data: decryptedAvatarData, | 
		
	
		
			
				|  |  |  |  |       url: fileUrl, | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |     const newAvatarPath = upgraded.path; | 
		
	
		
			
				|  |  |  |  |     // Replace our temporary image with the attachment pointer from the server:
 | 
		
	
		
			
				|  |  |  |  |     ourConvo.set('avatar', null); | 
		
	
		
			
				|  |  |  |  |     const existingHash = ourConvo.get('avatarHash'); | 
		
	
		
			
				|  |  |  |  |     const displayName = ourConvo.get('profileName'); | 
		
	
		
			
				|  |  |  |  |     // this commits already
 | 
		
	
		
			
				|  |  |  |  |     await ourConvo.setLokiProfile({ avatar: newAvatarPath, displayName, avatarHash: existingHash }); | 
		
	
		
			
				|  |  |  |  |     const newTimestampReupload = Date.now(); | 
		
	
		
			
				|  |  |  |  |     await createOrUpdateItem({ id: lastAvatarUploadTimestamp, value: newTimestampReupload }); | 
		
	
		
			
				|  |  |  |  |     window.log.info( | 
		
	
		
			
				|  |  |  |  |       `Reuploading avatar finished at ${newTimestampReupload}, newAttachmentPointer ${fileUrl}` | 
		
	
		
			
				|  |  |  |  |     ); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | }; | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * This function is called only once: on app startup with a logged in user | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
	
		
			
				
					|  |  |  | @ -158,6 +256,7 @@ const doAppStartUp = (dispatch: Dispatch<any>) => { | 
		
	
		
			
				|  |  |  |  |     void OnionPaths.buildNewOnionPathsOneAtATime(); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   void showUnstableAttachmentsDialogIfNeeded(); | 
		
	
		
			
				|  |  |  |  |   // init the messageQueue. In the constructor, we add all not send messages
 | 
		
	
		
			
				|  |  |  |  |   // this call does nothing except calling the constructor, which will continue sending message in the pipeline
 | 
		
	
		
			
				|  |  |  |  |   void getMessageQueue().processAllPending(); | 
		
	
	
		
			
				
					|  |  |  | @ -178,6 +277,8 @@ const doAppStartUp = (dispatch: Dispatch<any>) => { | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   void loadDefaultRooms(); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   debounce(triggerAvatarReUploadIfNeeded, 200); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   // TODO: Investigate the case where we reconnect
 | 
		
	
		
			
				|  |  |  |  |   const ourKey = UserUtils.getOurPubKeyStrFromCache(); | 
		
	
		
			
				|  |  |  |  |   SwarmPolling.getInstance().addPubkey(ourKey); | 
		
	
	
		
			
				
					|  |  |  | @ -228,6 +329,11 @@ export const ActionsPanel = () => { | 
		
	
		
			
				|  |  |  |  |     void forceRefreshRandomSnodePool(); | 
		
	
		
			
				|  |  |  |  |   }, DAYS * 1); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   useInterval(() => { | 
		
	
		
			
				|  |  |  |  |     // this won't be run every days, but if the app stays open for more than 10 days
 | 
		
	
		
			
				|  |  |  |  |     void triggerAvatarReUploadIfNeeded(); | 
		
	
		
			
				|  |  |  |  |   }, DAYS * 1); | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   return ( | 
		
	
		
			
				|  |  |  |  |     <div className="module-left-pane__sections-container"> | 
		
	
		
			
				|  |  |  |  |       <Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} /> | 
		
	
	
		
			
				
					|  |  |  | 
 |