import { isEmpty , isNil } from 'lodash' ;
import { setLastProfileUpdateTimestamp } from '../../util/storage' ;
import { UserConfigWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface' ;
import { getConversationController } from '../conversations' ;
import { SyncUtils , UserUtils } from '../utils' ;
import { fromHexToArray , sanitizeSessionUsername , toHex } from '../utils/String' ;
import { AvatarDownload } from '../utils/job_runners/jobs/AvatarDownloadJob' ;
import { CONVERSATION_PRIORITIES , ConversationTypeEnum } from '../../models/types' ;
export type Profile = {
displayName : string | undefined ;
profileUrl : string | null ;
profileKey : Uint8Array | null ;
priority : number | null ; // passing null means to not update the priority at all (used for legacy config message for now)
} ;
/ * *
* This can be used to update our conversation display name with the given name right away , and plan an AvatarDownloadJob to retrieve the new avatar if needed to download it
* /
async function updateOurProfileSync ( { displayName , profileUrl , profileKey , priority } : Profile ) {
const us = UserUtils . getOurPubKeyStrFromCache ( ) ;
const ourConvo = getConversationController ( ) . get ( us ) ;
if ( ! ourConvo ? . id ) {
window ? . log ? . warn ( '[profileupdate] Cannot update our profile without convo associated' ) ;
return ;
}
await updateProfileOfContact ( us , displayName , profileUrl , profileKey ) ;
if ( priority !== null && ourConvo . get ( 'priority' ) !== priority ) {
ourConvo . set ( 'priority' , priority ) ;
await ourConvo . commit ( ) ;
}
}
/ * *
* This can be used to update the display name of the given pubkey right away , and plan an AvatarDownloadJob to retrieve the new avatar if needed to download it .
* /
async function updateProfileOfContact (
pubkey : string ,
displayName : string | null | undefined ,
profileUrl : string | null | undefined ,
profileKey : Uint8Array | null | undefined
) {
const conversation = getConversationController ( ) . get ( pubkey ) ;
// TODO we should make sure that this function does not get call directly when `updateOurProfileSync` should be called instead. I.e. for avatars received in messages from ourself
if ( ! conversation || ! conversation . isPrivate ( ) ) {
window . log . warn ( 'updateProfileOfContact can only be used for existing and private convos' ) ;
return ;
}
let changes = false ;
const existingDisplayName = conversation . get ( 'displayNameInProfile' ) ;
// avoid setting the display name to an invalid value
if ( existingDisplayName !== displayName && ! isEmpty ( displayName ) ) {
conversation . set ( 'displayNameInProfile' , displayName || undefined ) ;
changes = true ;
}
const profileKeyHex = ! profileKey || isEmpty ( profileKey ) ? null : toHex ( profileKey ) ;
let avatarChanged = false ;
// trust whatever we get as an update. It either comes from a shared config wrapper or one of that user's message. But in any case we should trust it, even if it gets resetted.
const prevPointer = conversation . get ( 'avatarPointer' ) ;
const prevProfileKey = conversation . get ( 'profileKey' ) ;
// we have to set it right away and not in the async download job, as the next .commit will save it to the
// database and wrapper (and we do not want to override anything in the wrapper's content
// with what we have locally, so we need the commit to have already the right values in pointer and profileKey)
if ( prevPointer !== profileUrl || prevProfileKey !== profileKeyHex ) {
conversation . set ( {
avatarPointer : profileUrl || undefined ,
profileKey : profileKeyHex || undefined ,
} ) ;
// if the avatar data we had before is not the same of what we received, we need to schedule a new avatar download job.
avatarChanged = true ; // allow changes from strings to null/undefined to trigger a AvatarDownloadJob. If that happens, we want to remove the local attachment file.
}
// if we have a local path to an downloaded avatar, but no corresponding url/key for it, it means that
// the avatar was most likely removed so let's remove our link to that file.
if ( ( ! profileUrl || ! profileKeyHex ) && conversation . get ( 'avatarInProfile' ) ) {
conversation . set ( { avatarInProfile : undefined } ) ;
changes = true ;
}
if ( changes ) {
await conversation . commit ( ) ;
}
if ( avatarChanged ) {
// this call will download the new avatar or reset the local filepath if needed
await AvatarDownload . addAvatarDownloadJob ( {
conversationId : pubkey ,
} ) ;
}
}
/ * *
* This will throw if the display name given is too long .
* When registering a user / linking a device , we want to enforce a limit on the displayName length .
* That limit is enforced by libsession when calling ` setName ` on the ` UserConfigWrapper ` .
* ` updateOurProfileDisplayNameOnboarding ` is used to create a temporary ` UserConfigWrapper ` , call ` setName ` on it and release the memory used by the wrapper .
* @returns the set displayName set if no error where thrown .
* /
async function updateOurProfileDisplayNameOnboarding ( newName : string ) {
const cleanName = sanitizeSessionUsername ( newName ) . trim ( ) ;
try {
// create a temp user config wrapper to test the display name with libsession
const privKey = new Uint8Array ( 64 ) ;
crypto . getRandomValues ( privKey ) ;
await UserConfigWrapperActions . init ( privKey , null ) ;
// this throws if the name is too long
await UserConfigWrapperActions . setName ( cleanName ) ;
const appliedName = await UserConfigWrapperActions . getName ( ) ;
if ( isNil ( appliedName ) ) {
throw new Error (
'updateOurProfileDisplayNameOnboarding failed to retrieve name after setting it'
) ;
}
return appliedName ;
} finally {
await UserConfigWrapperActions . free ( ) ;
}
}
async function updateOurProfileDisplayName ( newName : string ) {
const ourNumber = UserUtils . getOurPubKeyStrFromCache ( ) ;
const conversation = await getConversationController ( ) . getOrCreateAndWait (
ourNumber ,
ConversationTypeEnum . PRIVATE
) ;
const dbProfileUrl = conversation . get ( 'avatarPointer' ) ;
const dbProfileKey = conversation . get ( 'profileKey' )
? fromHexToArray ( conversation . get ( 'profileKey' ) ! )
: null ;
const dbPriority = conversation . get ( 'priority' ) || CONVERSATION_PRIORITIES . default ;
// we don't want to throw if somehow our display name in the DB is too long here, so we use the truncated version.
await UserConfigWrapperActions . setNameTruncated ( sanitizeSessionUsername ( newName ) . trim ( ) ) ;
const truncatedName = await UserConfigWrapperActions . getName ( ) ;
if ( isNil ( truncatedName ) ) {
throw new Error ( 'updateOurProfileDisplayName: failed to get truncated displayName back' ) ;
}
await UserConfigWrapperActions . setPriority ( dbPriority ) ;
if ( dbProfileUrl && ! isEmpty ( dbProfileKey ) ) {
await UserConfigWrapperActions . setProfilePic ( { key : dbProfileKey , url : dbProfileUrl } ) ;
} else {
await UserConfigWrapperActions . setProfilePic ( { key : null , url : null } ) ;
}
conversation . setSessionDisplayNameNoCommit ( truncatedName ) ;
// might be good to not trigger a sync if the name did not change
await conversation . commit ( ) ;
await setLastProfileUpdateTimestamp ( Date . now ( ) ) ;
await SyncUtils . forceSyncConfigurationNowIfNeeded ( true ) ;
return truncatedName ;
}
export const ProfileManager = {
updateOurProfileSync ,
updateProfileOfContact ,
updateOurProfileDisplayName ,
updateOurProfileDisplayNameOnboarding ,
} ;