mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
716 lines
23 KiB
TypeScript
716 lines
23 KiB
TypeScript
import { isEmpty, isEqual, isNil, isString } from 'lodash';
|
|
import { to_hex } from 'libsodium-wrappers-sumo';
|
|
import type {
|
|
ContactInfoGet,
|
|
ConvoInfoVolatileGet1o1,
|
|
DecodedPro,
|
|
GroupMemberGet,
|
|
} from 'libsession_util_nodejs';
|
|
|
|
import type { ConversationModel } from './conversation';
|
|
import type {
|
|
WithAvatarPathAndFallback,
|
|
WithAvatarPointerProfileKey,
|
|
} from './conversationAttributes';
|
|
import { ed25519Str } from '../session/utils/String';
|
|
import { Timestamp } from '../types/timestamp/timestamp';
|
|
import { privateSet, privateSetKey } from './modelFriends';
|
|
import type { SignalService } from '../protobuf';
|
|
import { getCachedUserConfig } from '../webworker/workers/browser/libsession/libsession_worker_userconfig_interface';
|
|
import type { SwarmDecodedEnvelope } from '../receiver/types';
|
|
|
|
type SessionProfileArgs = {
|
|
displayName: string;
|
|
profileUpdatedAtSeconds: number;
|
|
convo: ConversationModel;
|
|
};
|
|
|
|
type SetSessionProfileReturn = {
|
|
nameChanged: boolean;
|
|
avatarChanged: boolean;
|
|
avatarNeedsDownload: boolean;
|
|
proDetailsChanged: boolean;
|
|
};
|
|
|
|
type WithConvo = { convo: SessionProfileArgs['convo'] };
|
|
type WithProfileUpdatedAtSeconds = {
|
|
profileUpdatedAtSeconds: SessionProfileArgs['profileUpdatedAtSeconds'];
|
|
};
|
|
|
|
type WithOptionalName = {
|
|
displayName: string | undefined | null;
|
|
};
|
|
|
|
type ProDetailsContact = {
|
|
proGenIndexHashB64: string | null;
|
|
proExpiryTsMs: number | null;
|
|
bitsetProFeatures: bigint | string | null;
|
|
};
|
|
|
|
type WithProDetailsContact = { proDetails: ProDetailsContact };
|
|
|
|
const debugProfileApplyChange = false;
|
|
|
|
abstract class SessionProfileChanges {
|
|
protected readonly convo: ConversationModel;
|
|
protected readonly displayName: WithOptionalName['displayName'];
|
|
|
|
public abstract applyChangesIfNeeded(): Promise<SetSessionProfileReturn>;
|
|
|
|
constructor(
|
|
args: WithConvo & {
|
|
displayName: string | undefined | null;
|
|
}
|
|
) {
|
|
this.convo = args.convo;
|
|
this.displayName = args.displayName;
|
|
}
|
|
|
|
public getConvoId() {
|
|
return this.convo.id;
|
|
}
|
|
|
|
public getDisplayName() {
|
|
return this.displayName;
|
|
}
|
|
|
|
protected assertConvoIsPrivate(identifier: string) {
|
|
if (!this.convo.isPrivate()) {
|
|
throw new Error(`${identifier}: expected a private conversations`);
|
|
}
|
|
}
|
|
|
|
protected assertConvoIsGroupV2OrCommunity(identifier: string) {
|
|
if (!this.convo.isClosedGroupV2() && !this.convo.isOpenGroupV2()) {
|
|
throw new Error(`${identifier}: expected a group v2 conversations or a community`);
|
|
}
|
|
}
|
|
|
|
protected assertConvoIsGroupV2(identifier: string) {
|
|
if (!this.convo.isClosedGroupV2()) {
|
|
throw new Error(`${identifier}: expected a group v2 conversations`);
|
|
}
|
|
}
|
|
|
|
protected shouldApplyChange(
|
|
newProfileUpdatedAtSeconds: number | null,
|
|
context:
|
|
| 'applyNameChange'
|
|
| 'applyUpdateAtChanges'
|
|
| 'applyResetAvatarChanges'
|
|
| 'applyProDetailsChange'
|
|
| 'applySetAvatarBeforeDownloadChanges'
|
|
) {
|
|
if (isNil(newProfileUpdatedAtSeconds)) {
|
|
// `null` is not an option to construct the subclasses of this class.
|
|
// So we use `null` to mean `profileUpdatedAtSeconds` does not apply to the subclass calling this.
|
|
// For instance, groups and communities do not need to check the profileUpdatedAtSeconds.
|
|
return true;
|
|
}
|
|
if (!this.convo.isPrivate()) {
|
|
// for non private shouldApplyChange calls, we do not need to check the profileUpdatedAtSeconds
|
|
|
|
return true;
|
|
}
|
|
const currentProfileUpdatedAtSeconds = this.convo.getProfileUpdatedSeconds();
|
|
// For the transition period, we need to allow an incoming profile to be applied when
|
|
// the timestamp is not set (defaults to 0).
|
|
if (newProfileUpdatedAtSeconds === 0 && currentProfileUpdatedAtSeconds === 0) {
|
|
if (debugProfileApplyChange) {
|
|
window.log.debug(
|
|
`shouldApplyChange [${context}] for ${ed25519Str(this.convo.id)} incomingSeconds:0 currentSeconds:0. Allowing overwrite`
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const ts = new Timestamp({ value: newProfileUpdatedAtSeconds });
|
|
if (debugProfileApplyChange) {
|
|
window.log.debug(
|
|
`shouldApplyChange [${context}] for ${ed25519Str(this.convo.id)} incomingSeconds:${ts.seconds()} currentSeconds:${currentProfileUpdatedAtSeconds} -> ${currentProfileUpdatedAtSeconds < ts.seconds()}`
|
|
);
|
|
}
|
|
return currentProfileUpdatedAtSeconds < ts.seconds();
|
|
}
|
|
|
|
protected applyNameChange(newProfileUpdatedAtSeconds: number | null) {
|
|
let nameChanged = false;
|
|
if (!this.shouldApplyChange(newProfileUpdatedAtSeconds, 'applyNameChange')) {
|
|
return { nameChanged };
|
|
}
|
|
const existingSessionName = this.convo.getRealSessionUsername();
|
|
|
|
if (this.displayName && this.displayName !== existingSessionName) {
|
|
this.convo[privateSet]({
|
|
displayNameInProfile: this.displayName,
|
|
});
|
|
|
|
nameChanged = true;
|
|
}
|
|
return { nameChanged };
|
|
}
|
|
|
|
protected applyUpdateAtChanges(newProfileUpdatedAtSeconds: number | null) {
|
|
let updatedAtChanged = false;
|
|
if (!this.shouldApplyChange(newProfileUpdatedAtSeconds, 'applyUpdateAtChanges')) {
|
|
return { updatedAtChanged };
|
|
}
|
|
|
|
this.convo[privateSet]({
|
|
profileUpdatedSeconds: newProfileUpdatedAtSeconds ?? undefined,
|
|
});
|
|
updatedAtChanged = true;
|
|
|
|
return { updatedAtChanged };
|
|
}
|
|
|
|
protected applyResetAvatarChanges(newProfileUpdatedAtSeconds: number | null) {
|
|
let avatarChanged = false;
|
|
if (!this.shouldApplyChange(newProfileUpdatedAtSeconds, 'applyResetAvatarChanges')) {
|
|
return { avatarChanged };
|
|
}
|
|
if (
|
|
this.convo.getAvatarInProfilePath() ||
|
|
this.convo.getFallbackAvatarInProfilePath() ||
|
|
this.convo.getAvatarPointer() ||
|
|
this.convo.getProfileKeyHex()
|
|
) {
|
|
this.convo[privateSet]({
|
|
avatarInProfile: undefined,
|
|
avatarPointer: undefined,
|
|
profileKey: undefined,
|
|
fallbackAvatarInProfile: undefined,
|
|
});
|
|
}
|
|
|
|
avatarChanged = true;
|
|
|
|
return { avatarChanged };
|
|
}
|
|
|
|
protected applyProDetailsChange(
|
|
{ bitsetProFeatures, proExpiryTsMs, proGenIndexHashB64 }: ProDetailsContact,
|
|
newProfileUpdatedAtSeconds: number | null
|
|
) {
|
|
let proDetailsChanged = false;
|
|
if (!this.shouldApplyChange(newProfileUpdatedAtSeconds, 'applyProDetailsChange')) {
|
|
return { proDetailsChanged };
|
|
}
|
|
|
|
if (this.convo.isMe()) {
|
|
// We don't want to set the pro details of ourself,
|
|
// as we can just use the ones provided from libsession directly.
|
|
return {
|
|
proDetailsChanged: false,
|
|
};
|
|
}
|
|
if (
|
|
!isNil(bitsetProFeatures) &&
|
|
this.convo.get('bitsetProFeatures') !== bitsetProFeatures.toString()
|
|
) {
|
|
this.convo[privateSetKey]('bitsetProFeatures', bitsetProFeatures.toString());
|
|
proDetailsChanged = true;
|
|
}
|
|
if (isNil(bitsetProFeatures) && this.convo.get('bitsetProFeatures')) {
|
|
this.convo[privateSetKey]('bitsetProFeatures', undefined);
|
|
proDetailsChanged = true;
|
|
}
|
|
if (!isNil(proGenIndexHashB64) && this.convo.get('proGenIndexHashB64') !== proGenIndexHashB64) {
|
|
this.convo[privateSetKey]('proGenIndexHashB64', proGenIndexHashB64);
|
|
proDetailsChanged = true;
|
|
}
|
|
if (!isNil(proExpiryTsMs) && this.convo.get('proExpiryTsMs') !== proExpiryTsMs) {
|
|
this.convo[privateSetKey]('proExpiryTsMs', proExpiryTsMs);
|
|
proDetailsChanged = true;
|
|
}
|
|
|
|
proDetailsChanged = true;
|
|
|
|
return { proDetailsChanged };
|
|
}
|
|
|
|
protected applySetAvatarBeforeDownloadChanges(
|
|
args: WithAvatarPointerProfileKey,
|
|
newProfileUpdatedAtSeconds: number | null
|
|
): Pick<SetSessionProfileReturn, 'avatarNeedsDownload' | 'avatarChanged'> {
|
|
if (
|
|
!this.shouldApplyChange(newProfileUpdatedAtSeconds, 'applySetAvatarBeforeDownloadChanges')
|
|
) {
|
|
return { avatarNeedsDownload: false, avatarChanged: false };
|
|
}
|
|
const newProfileKeyHex = isString(args.profileKey) ? args.profileKey : to_hex(args.profileKey);
|
|
|
|
const existingAvatarPointer = this.convo.getAvatarPointer();
|
|
const existingProfileKeyHex = this.convo.getProfileKeyHex();
|
|
const hasAvatarInNewProfile = !!args.avatarPointer || !!newProfileKeyHex;
|
|
// if no changes are needed, return early
|
|
if (
|
|
isEqual(existingAvatarPointer, args.avatarPointer) &&
|
|
isEqual(existingProfileKeyHex, newProfileKeyHex)
|
|
) {
|
|
return { avatarNeedsDownload: false, avatarChanged: false };
|
|
}
|
|
this.convo[privateSet]({ avatarPointer: args.avatarPointer, profileKey: newProfileKeyHex });
|
|
|
|
return { avatarNeedsDownload: hasAvatarInNewProfile, avatarChanged: true };
|
|
}
|
|
|
|
protected applySetAvatarDownloadedChanges(
|
|
args: WithAvatarPointerProfileKey & WithAvatarPathAndFallback
|
|
): Pick<SetSessionProfileReturn, 'avatarChanged'> {
|
|
// Note: this function does not need the `profileUpdatedAtSeconds` logic for any types of convo
|
|
const newProfileKeyHex = isString(args.profileKey) ? args.profileKey : to_hex(args.profileKey);
|
|
|
|
const existingAvatarPointer = this.convo.getAvatarPointer();
|
|
const existingProfileKeyHex = this.convo.getProfileKeyHex();
|
|
const originalAvatar = this.convo.getAvatarInProfilePath();
|
|
const originalFallbackAvatar = this.convo.getFallbackAvatarInProfilePath();
|
|
|
|
// if no changes are needed, return early
|
|
if (
|
|
isEqual(originalAvatar, args.avatarPath) &&
|
|
isEqual(originalFallbackAvatar, args.fallbackAvatarPath) &&
|
|
isEqual(existingAvatarPointer, args.avatarPointer) &&
|
|
isEqual(existingProfileKeyHex, newProfileKeyHex)
|
|
) {
|
|
return { avatarChanged: false };
|
|
}
|
|
// Avatar has changed, but we are only dealing with the downloaded part of it here.
|
|
// Hence why we do not deal with the profileUpdatedAtSeconds here.
|
|
|
|
this.convo[privateSet]({
|
|
avatarPointer: args.avatarPointer,
|
|
avatarInProfile: args.avatarPath,
|
|
fallbackAvatarInProfile: args.fallbackAvatarPath,
|
|
profileKey: newProfileKeyHex,
|
|
});
|
|
return { avatarChanged: true };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the display name of a private conversation.
|
|
*/
|
|
export class SessionDisplayNameOnlyPrivate extends SessionProfileChanges {
|
|
private readonly profileUpdatedAtSeconds: WithProfileUpdatedAtSeconds['profileUpdatedAtSeconds'];
|
|
|
|
constructor(
|
|
args: WithConvo &
|
|
WithProfileUpdatedAtSeconds & {
|
|
/**
|
|
* As part of `SessionDisplayNameOnlyPrivate`, we do require the name to be set,
|
|
* so we cannot reuse `WithOptionalName` as is here
|
|
*/
|
|
displayName: NonNullable<WithOptionalName['displayName']>;
|
|
}
|
|
) {
|
|
super(args);
|
|
this.assertConvoIsPrivate('SessionDisplayNameOnlyPrivate');
|
|
|
|
this.profileUpdatedAtSeconds = args.profileUpdatedAtSeconds;
|
|
}
|
|
|
|
async applyChangesIfNeeded(): Promise<SetSessionProfileReturn> {
|
|
const { nameChanged } = this.applyNameChange(this.profileUpdatedAtSeconds);
|
|
if (nameChanged) {
|
|
this.applyUpdateAtChanges(this.profileUpdatedAtSeconds);
|
|
await this.convo.commit();
|
|
}
|
|
|
|
return {
|
|
nameChanged,
|
|
avatarNeedsDownload: false,
|
|
avatarChanged: false,
|
|
// Note: SessionDisplayNameOnlyPrivate is currently only used for our NTS convo and we don't keep
|
|
// the pro details here in that case.
|
|
proDetailsChanged: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the avatar of a private conversation.
|
|
* This is the only reset case where we need the `profileUpdatedAtSeconds`.
|
|
*/
|
|
export class SessionProfileResetAvatarPrivate extends SessionProfileChanges {
|
|
private readonly profileUpdatedAtSeconds: WithProfileUpdatedAtSeconds['profileUpdatedAtSeconds'];
|
|
private readonly proDetails: ProDetailsContact;
|
|
|
|
constructor(
|
|
args: WithConvo & WithProfileUpdatedAtSeconds & WithOptionalName & WithProDetailsContact
|
|
) {
|
|
super(args);
|
|
this.assertConvoIsPrivate('SessionProfileResetAvatarPrivate');
|
|
|
|
this.profileUpdatedAtSeconds = args.profileUpdatedAtSeconds;
|
|
this.proDetails = args.proDetails;
|
|
}
|
|
|
|
async applyChangesIfNeeded(): Promise<SetSessionProfileReturn> {
|
|
const { nameChanged } = this.applyNameChange(this.profileUpdatedAtSeconds);
|
|
const { avatarChanged } = this.applyResetAvatarChanges(this.profileUpdatedAtSeconds);
|
|
const { proDetailsChanged } = this.applyProDetailsChange(
|
|
this.proDetails,
|
|
this.profileUpdatedAtSeconds
|
|
);
|
|
if (avatarChanged || nameChanged || proDetailsChanged) {
|
|
this.applyUpdateAtChanges(this.profileUpdatedAtSeconds);
|
|
await this.convo.commit();
|
|
}
|
|
|
|
return {
|
|
nameChanged,
|
|
avatarNeedsDownload: false,
|
|
avatarChanged,
|
|
proDetailsChanged,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the avatar of a group or community conversation.
|
|
* We do not need the `profileUpdatedAtSeconds` here.
|
|
*/
|
|
export class SessionProfileResetAvatarGroupCommunity extends SessionProfileChanges {
|
|
constructor(args: WithConvo & WithOptionalName) {
|
|
super(args);
|
|
this.assertConvoIsGroupV2OrCommunity('SessionProfileResetAvatarGroupCommunity');
|
|
}
|
|
|
|
async applyChangesIfNeeded(): Promise<SetSessionProfileReturn> {
|
|
const { nameChanged } = this.applyNameChange(null);
|
|
|
|
const { avatarChanged } = this.applyResetAvatarChanges(null);
|
|
if (avatarChanged || nameChanged) {
|
|
this.applyUpdateAtChanges(null);
|
|
await this.convo.commit();
|
|
}
|
|
|
|
return {
|
|
nameChanged,
|
|
avatarChanged,
|
|
avatarNeedsDownload: false,
|
|
proDetailsChanged: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the avatar of a private conversation before downloading it.
|
|
* Note: for communities, we set & download the avatar as a single step
|
|
*/
|
|
export class SessionProfileSetAvatarBeforeDownloadPrivate extends SessionProfileChanges {
|
|
private readonly profileUpdatedAtSeconds: WithProfileUpdatedAtSeconds['profileUpdatedAtSeconds'];
|
|
private readonly extraArgs: WithAvatarPointerProfileKey & WithProDetailsContact;
|
|
|
|
constructor(
|
|
args: WithConvo &
|
|
WithOptionalName &
|
|
WithProfileUpdatedAtSeconds &
|
|
WithAvatarPointerProfileKey &
|
|
WithProDetailsContact
|
|
) {
|
|
super(args);
|
|
|
|
this.assertConvoIsPrivate('SessionProfileSetAvatarBeforeDownloadPrivate');
|
|
this.profileUpdatedAtSeconds = args.profileUpdatedAtSeconds;
|
|
this.extraArgs = args;
|
|
}
|
|
|
|
async applyChangesIfNeeded(): Promise<SetSessionProfileReturn> {
|
|
const { nameChanged } = this.applyNameChange(this.profileUpdatedAtSeconds);
|
|
|
|
const { avatarNeedsDownload, avatarChanged } = this.applySetAvatarBeforeDownloadChanges(
|
|
this.extraArgs,
|
|
this.profileUpdatedAtSeconds
|
|
);
|
|
const { proDetailsChanged } = this.applyProDetailsChange(
|
|
this.extraArgs.proDetails,
|
|
this.profileUpdatedAtSeconds
|
|
);
|
|
if (avatarNeedsDownload || avatarChanged || nameChanged || proDetailsChanged) {
|
|
this.applyUpdateAtChanges(this.profileUpdatedAtSeconds);
|
|
await this.convo.commit();
|
|
}
|
|
|
|
return {
|
|
nameChanged,
|
|
avatarNeedsDownload,
|
|
avatarChanged,
|
|
proDetailsChanged,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the avatar of a group conversation before downloading it.
|
|
* This one doesn't need the profileUpdatedAtSeconds.
|
|
*/
|
|
export class SessionProfileSetAvatarBeforeDownloadGroup extends SessionProfileChanges {
|
|
private readonly extraArgs: WithAvatarPointerProfileKey;
|
|
|
|
constructor(args: WithConvo & WithOptionalName & WithAvatarPointerProfileKey) {
|
|
super(args);
|
|
this.assertConvoIsGroupV2('SessionProfileSetAvatarBeforeDownloadGroup');
|
|
this.extraArgs = args;
|
|
}
|
|
|
|
async applyChangesIfNeeded(): Promise<SetSessionProfileReturn> {
|
|
const { nameChanged } = this.applyNameChange(null);
|
|
|
|
const { avatarNeedsDownload, avatarChanged } = this.applySetAvatarBeforeDownloadChanges(
|
|
this.extraArgs,
|
|
null // group have no updatedAtSeconds logic
|
|
);
|
|
if (avatarNeedsDownload || avatarChanged || nameChanged) {
|
|
this.applyUpdateAtChanges(null);
|
|
await this.convo.commit();
|
|
}
|
|
|
|
return {
|
|
nameChanged,
|
|
avatarNeedsDownload,
|
|
avatarChanged,
|
|
proDetailsChanged: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the avatar of a group conversation before downloading it.
|
|
* This one doesn't need the profileUpdatedAtSeconds.
|
|
*/
|
|
export class SessionProfileSetAvatarDownloadedAny extends SessionProfileChanges {
|
|
private readonly extraArgs: WithAvatarPointerProfileKey & WithAvatarPathAndFallback;
|
|
|
|
constructor(
|
|
args: WithConvo & WithAvatarPointerProfileKey & WithAvatarPathAndFallback & WithOptionalName
|
|
) {
|
|
super(args);
|
|
// No validate of the convo type here, as we support this change for any type of convo.
|
|
this.extraArgs = args;
|
|
}
|
|
|
|
async applyChangesIfNeeded(): Promise<SetSessionProfileReturn> {
|
|
const { nameChanged } = this.applyNameChange(null);
|
|
|
|
const { avatarChanged } = this.applySetAvatarDownloadedChanges(this.extraArgs);
|
|
if (avatarChanged || nameChanged) {
|
|
this.applyUpdateAtChanges(null);
|
|
await this.convo.commit();
|
|
}
|
|
|
|
return {
|
|
nameChanged,
|
|
avatarNeedsDownload: false,
|
|
avatarChanged,
|
|
proDetailsChanged: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
const emptyProDetails = {
|
|
proDetails: {
|
|
bitsetProFeatures: null,
|
|
proExpiryTsMs: null,
|
|
proGenIndexHashB64: null,
|
|
},
|
|
};
|
|
|
|
function buildProProfileDetailsFromContact({
|
|
contact,
|
|
convoVolatileDetails,
|
|
}: {
|
|
contact: ContactInfoGet;
|
|
convoVolatileDetails: ConvoInfoVolatileGet1o1 | null;
|
|
}): WithProDetailsContact {
|
|
return {
|
|
proDetails: {
|
|
bitsetProFeatures: contact?.proProfileBitset ?? null,
|
|
proExpiryTsMs: convoVolatileDetails?.proExpiryTsMs ?? null,
|
|
proGenIndexHashB64: convoVolatileDetails?.genIndexHashB64 ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildProProfileDetailsFromEnvelope({
|
|
decodedEnvelope,
|
|
}: {
|
|
decodedEnvelope: SwarmDecodedEnvelope;
|
|
}): WithProDetailsContact {
|
|
if (!decodedEnvelope.validPro) {
|
|
return emptyProDetails;
|
|
}
|
|
if (
|
|
!decodedEnvelope.validPro.proProfileBitset ||
|
|
!decodedEnvelope.validPro.proProof.genIndexHashB64
|
|
) {
|
|
return emptyProDetails;
|
|
}
|
|
const isValidOrExpired = decodedEnvelope.isProProofValidOrExpired();
|
|
|
|
return isValidOrExpired
|
|
? {
|
|
proDetails: {
|
|
bitsetProFeatures: decodedEnvelope.validPro.proProfileBitset,
|
|
proExpiryTsMs: decodedEnvelope.validPro.proProof.expiryMs,
|
|
proGenIndexHashB64: decodedEnvelope.validPro.proProof.genIndexHashB64,
|
|
},
|
|
}
|
|
: emptyProDetails;
|
|
}
|
|
|
|
/**
|
|
* Build a private profile change from a message request response & pro details.
|
|
*/
|
|
export function buildPrivateProfileChangeFromMsgRequestResponse({
|
|
convo,
|
|
messageRequestResponse,
|
|
decodedEnvelope,
|
|
}: WithConvo & {
|
|
messageRequestResponse: SignalService.MessageRequestResponse;
|
|
decodedEnvelope: SwarmDecodedEnvelope;
|
|
}) {
|
|
if (isEmpty(messageRequestResponse.profile)) {
|
|
return null;
|
|
}
|
|
|
|
const shared = {
|
|
convo,
|
|
displayName: messageRequestResponse.profile.displayName,
|
|
profileUpdatedAtSeconds: new Timestamp({
|
|
value: messageRequestResponse.profile.lastProfileUpdateSeconds ?? 0,
|
|
}).seconds(),
|
|
...buildProProfileDetailsFromEnvelope({ decodedEnvelope }),
|
|
};
|
|
|
|
if (messageRequestResponse.profileKey && messageRequestResponse.profile.profilePicture) {
|
|
return new SessionProfileSetAvatarBeforeDownloadPrivate({
|
|
profileKey: messageRequestResponse.profileKey,
|
|
avatarPointer: messageRequestResponse.profile.profilePicture,
|
|
...shared,
|
|
});
|
|
}
|
|
return new SessionProfileResetAvatarPrivate(shared);
|
|
}
|
|
|
|
export function buildPrivateProfileChangeFromMetaGroupMember({
|
|
convo,
|
|
member,
|
|
}: WithConvo & {
|
|
member: GroupMemberGet;
|
|
}) {
|
|
const shared = {
|
|
convo,
|
|
displayName: member.name,
|
|
profileUpdatedAtSeconds: member.profileUpdatedSeconds,
|
|
proDetails: {
|
|
// Pass null to the fields so we don't overwrite them.
|
|
bitsetProFeatures: null, // The member object has no pro details.
|
|
proExpiryTsMs: null, // The member object has no pro details.
|
|
proGenIndexHashB64: null, // The member object has no pro details.
|
|
},
|
|
};
|
|
if (member.profilePicture?.url && member.profilePicture?.key) {
|
|
return new SessionProfileSetAvatarBeforeDownloadPrivate({
|
|
avatarPointer: member.profilePicture.url,
|
|
profileKey: member.profilePicture.key,
|
|
...shared,
|
|
});
|
|
}
|
|
|
|
return new SessionProfileResetAvatarPrivate(shared);
|
|
}
|
|
|
|
export function buildPrivateProfileChangeFromContactUpdate({
|
|
contact,
|
|
convo,
|
|
convoVolatileDetails,
|
|
}: WithConvo & {
|
|
contact: ContactInfoGet;
|
|
convoVolatileDetails: ConvoInfoVolatileGet1o1 | null;
|
|
}) {
|
|
const proProfileDetails = buildProProfileDetailsFromContact({ contact, convoVolatileDetails });
|
|
const shared = {
|
|
convo,
|
|
displayName: contact.name,
|
|
profileUpdatedAtSeconds: contact.profileUpdatedSeconds,
|
|
...proProfileDetails,
|
|
};
|
|
if (contact.profilePicture?.url && contact.profilePicture?.key) {
|
|
return new SessionProfileSetAvatarBeforeDownloadPrivate({
|
|
profileKey: contact.profilePicture?.key,
|
|
avatarPointer: contact.profilePicture?.url,
|
|
...shared,
|
|
});
|
|
}
|
|
return new SessionProfileResetAvatarPrivate(shared);
|
|
}
|
|
|
|
export function buildPrivateProfileChangeFromSwarmDataMessage({
|
|
dataMessage,
|
|
decodedPro,
|
|
convo,
|
|
}: WithConvo & {
|
|
dataMessage: SignalService.DataMessage;
|
|
decodedPro: DecodedPro | null;
|
|
}) {
|
|
if (!dataMessage.profile || isEmpty(dataMessage.profile)) {
|
|
return null;
|
|
}
|
|
const shared = {
|
|
convo,
|
|
displayName: dataMessage.profile.displayName,
|
|
profileUpdatedAtSeconds: new Timestamp({
|
|
value: dataMessage.profile.lastProfileUpdateSeconds ?? 0,
|
|
}).seconds(),
|
|
proDetails: {
|
|
bitsetProFeatures: decodedPro?.proProfileBitset ?? null,
|
|
proExpiryTsMs: decodedPro?.proProof.expiryMs ?? null,
|
|
proGenIndexHashB64: decodedPro?.proProof.genIndexHashB64 ?? null,
|
|
},
|
|
};
|
|
|
|
return dataMessage.profileKey && dataMessage.profile.profilePicture
|
|
? new SessionProfileSetAvatarBeforeDownloadPrivate({
|
|
profileKey: dataMessage.profileKey,
|
|
avatarPointer: dataMessage.profile.profilePicture,
|
|
...shared,
|
|
})
|
|
: new SessionProfileResetAvatarPrivate({
|
|
...shared,
|
|
});
|
|
}
|
|
|
|
export async function buildPrivateProfileChangeFromUserProfileUpdate(ourConvo: ConversationModel) {
|
|
const {
|
|
profilePic,
|
|
name: displayName,
|
|
profileUpdatedSeconds: profileUpdatedAtSeconds,
|
|
} = getCachedUserConfig();
|
|
|
|
const shared = {
|
|
convo: ourConvo,
|
|
displayName,
|
|
profileUpdatedAtSeconds,
|
|
proDetails: {
|
|
bitsetProFeatures: null, // NTS case, we don't care about those
|
|
proExpiryTsMs: null, // NTS case, we don't care about those
|
|
proGenIndexHashB64: null, // NTS case, we don't care about those
|
|
},
|
|
};
|
|
return profilePic.url && profilePic.key
|
|
? new SessionProfileSetAvatarBeforeDownloadPrivate({
|
|
profileKey: profilePic.key,
|
|
avatarPointer: profilePic.url,
|
|
...shared,
|
|
})
|
|
: new SessionProfileResetAvatarPrivate(shared);
|
|
}
|
|
|
|
export type SessionProfilePrivateChange =
|
|
| SessionDisplayNameOnlyPrivate
|
|
| SessionProfileSetAvatarBeforeDownloadPrivate
|
|
| SessionProfileResetAvatarPrivate
|
|
| SessionProfileSetAvatarDownloadedAny;
|