Skip to content

Commit bc8eb93

Browse files
committed
feat: added tracking of profileUpdate for contacts/groups members
1 parent 1cb680e commit bc8eb93

File tree

25 files changed

+440
-283
lines changed

25 files changed

+440
-283
lines changed

ts/components/dialog/debug/components.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { releasedFeaturesActions } from '../../../state/ducks/releasedFeatures';
3535
import { networkDataActions } from '../../../state/ducks/networkData';
3636
import { DEBUG_MENU_PAGE, type DebugMenuPageProps } from './DebugMenuModal';
3737
import { SimpleSessionInput } from '../../inputs/SessionInput';
38+
import { NetworkTime } from '../../../util/NetworkTime';
3839

3940
const hexRef = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
4041

@@ -59,7 +60,11 @@ async function generateOneRandomContact() {
5960
// for it to be inserted in the config
6061
created.setActiveAt(Date.now());
6162
await created.setIsApproved(true, false);
62-
created.setSessionDisplayNameNoCommit(id.slice(2, 8));
63+
await created.setSessionProfile({
64+
type: 'displayNameChangeOnlyPrivate',
65+
displayName: id.slice(2, 8),
66+
profileUpdatedAtSeconds: NetworkTime.nowSeconds(),
67+
});
6368

6469
await created.commit();
6570
return created;

ts/interactions/avatar-interactions/nts-avatar-interactions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export async function uploadAndSetOurAvatarShared({
115115
fallbackAvatarPath: processedFallbackAvatar?.path || savedMainAvatar.path,
116116
displayName,
117117
avatarPointer: fileUrl,
118-
type: 'setAvatarDownloaded',
118+
type: 'setAvatarDownloadedPrivate',
119119
profileKey,
120120
});
121121
await Storage.put(SettingsKey.ntsAvatarExpiryMs, expiresMs);

ts/models/conversation.ts

Lines changed: 176 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ import {
101101
isOpenOrClosedGroup,
102102
READ_MESSAGE_STATE,
103103
type ConversationNotificationSettingType,
104+
type WithAvatarPathAndFallback,
105+
type WithAvatarPointer,
106+
type WithAvatarPointerProfileKey,
107+
type WithProfileKey,
108+
type WithProfileUpdatedAtSeconds,
104109
} from './conversationAttributes';
105110

106111
import { ReadReceiptMessage } from '../session/messages/outgoing/controlMessage/receipt/ReadReceiptMessage';
@@ -150,12 +155,66 @@ import type {
150155
} from '../interactions/types';
151156
import type { LastMessageStatusType } from '../state/ducks/types';
152157
import { OutgoingUserProfile } from '../types/message';
158+
import { Timestamp } from '../types/timestamp/timestamp';
153159

154160
type InMemoryConvoInfos = {
155161
mentionedUs: boolean;
156162
unreadCount: number;
157163
};
158164

165+
type WithSetSessionProfileType<
166+
T extends
167+
| 'displayNameChangeOnlyPrivate'
168+
| 'resetAvatarPrivate'
169+
| 'resetAvatarGroup'
170+
| 'resetAvatarCommunity'
171+
| 'setAvatarBeforeDownloadPrivate'
172+
| 'setAvatarBeforeDownloadGroup'
173+
| 'setAvatarDownloadedPrivate'
174+
| 'setAvatarDownloadedCommunity'
175+
| 'setAvatarDownloadedGroup',
176+
> = {
177+
type: T;
178+
};
179+
180+
type WithDisplayNameChange = { displayName?: string | null };
181+
182+
type SetSessionProfileDetails = WithDisplayNameChange &
183+
(
184+
| (WithSetSessionProfileType<'displayNameChangeOnlyPrivate'> & WithProfileUpdatedAtSeconds)
185+
| (WithSetSessionProfileType<'resetAvatarPrivate'> & WithProfileUpdatedAtSeconds)
186+
// no need for profileUpdatedAtSeconds for groups/communities
187+
| WithSetSessionProfileType<'resetAvatarGroup' | 'resetAvatarCommunity'>
188+
// Note: for communities, we set & download the avatar as a single step
189+
| (WithSetSessionProfileType<'setAvatarBeforeDownloadPrivate'> &
190+
WithProfileUpdatedAtSeconds &
191+
WithAvatarPointerProfileKey)
192+
// no need for profileUpdatedAtSeconds for groups
193+
| (WithSetSessionProfileType<'setAvatarBeforeDownloadGroup'> &
194+
WithProfileKey &
195+
WithAvatarPointer) // no need for profileUpdatedAtSeconds for groups
196+
// Note: for communities, we set & download the avatar as a single step
197+
| (WithSetSessionProfileType<'setAvatarDownloadedPrivate'> &
198+
WithAvatarPointerProfileKey &
199+
WithAvatarPathAndFallback)
200+
201+
// no need for profileUpdatedAtSeconds for groups/communities
202+
| (WithSetSessionProfileType<'setAvatarDownloadedCommunity' | 'setAvatarDownloadedGroup'> &
203+
WithAvatarPointerProfileKey &
204+
WithAvatarPathAndFallback)
205+
);
206+
207+
/**
208+
*
209+
* Type guard for the set profile action that is private.
210+
* We need to do some extra processing for private actions, as they have a updatedAtSeconds field.
211+
*/
212+
function isSetProfileWithUpdatedAtSeconds<T extends SetSessionProfileDetails>(
213+
action: T
214+
): action is Extract<T, { profileUpdatedAtSeconds: number }> {
215+
return 'profileUpdatedAtSeconds' in action;
216+
}
217+
159218
/**
160219
* Some fields are not stored in the database, but are kept in memory.
161220
* We use this map to keep track of them. The key is the conversation id.
@@ -1406,117 +1465,140 @@ export class ConversationModel extends Model<ConversationAttributes> {
14061465
}
14071466
}
14081467

1468+
private shouldApplyPrivateProfileUpdate(newProfile: SetSessionProfileDetails) {
1469+
if (isSetProfileWithUpdatedAtSeconds(newProfile)) {
1470+
const ts = new Timestamp({ value: newProfile.profileUpdatedAtSeconds });
1471+
return this.getProfileUpdatedSeconds() < ts.seconds();
1472+
}
1473+
// for non private setProfile calls, we do not need to check the updatedAtSeconds
1474+
return true;
1475+
}
1476+
14091477
/**
14101478
* Updates this conversation with the provided displayName, avatarPath, staticAvatarPath and avatarPointer.
14111479
* - displayName can be set to null to not update the display name.
14121480
* - if any of the avatar fields is set, they all need to be set.
14131481
*
14141482
* This function does commit to the DB if any changes are detected.
14151483
*/
1416-
public async setSessionProfile(
1417-
newProfile: { displayName?: string | null } & (
1418-
| {
1419-
type: 'resetAvatar';
1420-
}
1421-
| {
1422-
type: 'setAvatarBeforeDownload';
1423-
avatarPointer: string;
1424-
profileKey: Uint8Array | string;
1425-
}
1426-
| {
1427-
type: 'setAvatarDownloaded';
1428-
avatarPath: string;
1429-
fallbackAvatarPath: string;
1430-
avatarPointer: string;
1431-
profileKey: Uint8Array | string;
1432-
}
1433-
)
1434-
) {
1435-
let changed = false;
1484+
public async setSessionProfile(newProfile: SetSessionProfileDetails): Promise<{
1485+
nameChanged: boolean;
1486+
avatarChanged: boolean;
1487+
avatarNeedsDownload: boolean;
1488+
}> {
1489+
let nameChanged = false;
14361490

14371491
const existingSessionName = this.getRealSessionUsername();
14381492
if (newProfile.displayName !== existingSessionName && newProfile.displayName) {
14391493
this.set({
14401494
displayNameInProfile: newProfile.displayName,
14411495
});
1442-
changed = true;
1496+
nameChanged = true;
14431497
}
1498+
const type = newProfile.type;
14441499

1445-
if (newProfile.type === 'resetAvatar') {
1446-
if (
1447-
this.getAvatarInProfilePath() ||
1448-
this.getFallbackAvatarInProfilePath() ||
1449-
this.getAvatarPointer() ||
1450-
this.getProfileKey()
1451-
) {
1452-
this.set({
1453-
avatarInProfile: undefined,
1454-
avatarPointer: undefined,
1455-
profileKey: undefined,
1456-
fallbackAvatarInProfile: undefined,
1457-
});
1458-
changed = true;
1459-
}
1460-
if (changed) {
1461-
await this.commit();
1500+
switch (type) {
1501+
case 'displayNameChangeOnlyPrivate': {
1502+
if (nameChanged) {
1503+
await this.commit();
1504+
}
1505+
return { nameChanged, avatarNeedsDownload: false, avatarChanged: false };
14621506
}
1463-
return changed;
1464-
}
1465-
1466-
const newProfileKeyHex = isString(newProfile.profileKey)
1467-
? newProfile.profileKey
1468-
: to_hex(newProfile.profileKey);
1469-
1470-
const existingAvatarPointer = this.getAvatarPointer();
1471-
const existingProfileKeyHex = this.getProfileKey();
1472-
1473-
if (newProfile.type === 'setAvatarBeforeDownload') {
1474-
// if no changes are needed, return early
1475-
if (
1476-
isEqual(existingAvatarPointer, newProfile.avatarPointer) &&
1477-
isEqual(existingProfileKeyHex, newProfileKeyHex)
1478-
) {
1479-
if (changed) {
1507+
case 'resetAvatarPrivate':
1508+
case 'resetAvatarGroup':
1509+
case 'resetAvatarCommunity': {
1510+
let avatarChanged = false;
1511+
if (
1512+
// When the avatar update is about a private one, we need to check
1513+
// if the profile has been updated more recently that what we have already
1514+
this.shouldApplyPrivateProfileUpdate(newProfile)
1515+
) {
1516+
if (
1517+
this.getAvatarInProfilePath() ||
1518+
this.getFallbackAvatarInProfilePath() ||
1519+
this.getAvatarPointer() ||
1520+
this.getProfileKey()
1521+
) {
1522+
this.set({
1523+
avatarInProfile: undefined,
1524+
avatarPointer: undefined,
1525+
profileKey: undefined,
1526+
fallbackAvatarInProfile: undefined,
1527+
});
1528+
avatarChanged = true;
1529+
}
1530+
}
1531+
if (avatarChanged) {
14801532
await this.commit();
14811533
}
1482-
return changed;
1534+
return { nameChanged, avatarNeedsDownload: false, avatarChanged };
14831535
}
1484-
this.set({ avatarPointer: newProfile.avatarPointer, profileKey: newProfileKeyHex });
14851536

1486-
await this.commit();
1537+
case 'setAvatarBeforeDownloadPrivate':
1538+
case 'setAvatarBeforeDownloadGroup': {
1539+
const newProfileKeyHex = isString(newProfile.profileKey)
1540+
? newProfile.profileKey
1541+
: to_hex(newProfile.profileKey);
1542+
1543+
const existingAvatarPointer = this.getAvatarPointer();
1544+
const existingProfileKeyHex = this.getProfileKey();
1545+
const hasAvatarInNewProfile = !!newProfile.avatarPointer || !!newProfileKeyHex;
1546+
// if no changes are needed, return early
1547+
if (
1548+
isEqual(existingAvatarPointer, newProfile.avatarPointer) &&
1549+
isEqual(existingProfileKeyHex, newProfileKeyHex)
1550+
) {
1551+
if (nameChanged) {
1552+
await this.commit();
1553+
}
1554+
return { nameChanged, avatarNeedsDownload: false, avatarChanged: false };
1555+
}
1556+
this.set({ avatarPointer: newProfile.avatarPointer, profileKey: newProfileKeyHex });
14871557

1488-
return true;
1489-
}
1558+
await this.commit();
14901559

1491-
if (newProfile.type !== 'setAvatarDownloaded') {
1492-
throw new Error('setSessionProfile: invalid type for avatar action');
1493-
}
1560+
return { nameChanged, avatarNeedsDownload: hasAvatarInNewProfile, avatarChanged: true };
1561+
}
1562+
case 'setAvatarDownloadedPrivate':
1563+
case 'setAvatarDownloadedGroup':
1564+
case 'setAvatarDownloadedCommunity': {
1565+
const newProfileKeyHex = isString(newProfile.profileKey)
1566+
? newProfile.profileKey
1567+
: to_hex(newProfile.profileKey);
1568+
1569+
const existingAvatarPointer = this.getAvatarPointer();
1570+
const existingProfileKeyHex = this.getProfileKey();
1571+
const originalAvatar = this.getAvatarInProfilePath();
1572+
const originalFallbackAvatar = this.getFallbackAvatarInProfilePath();
1573+
1574+
// if no changes are needed, return early
1575+
if (
1576+
isEqual(originalAvatar, newProfile.avatarPath) &&
1577+
isEqual(originalFallbackAvatar, newProfile.fallbackAvatarPath) &&
1578+
isEqual(existingAvatarPointer, newProfile.avatarPointer) &&
1579+
isEqual(existingProfileKeyHex, newProfileKeyHex)
1580+
) {
1581+
if (nameChanged) {
1582+
await this.commit();
1583+
}
1584+
return { nameChanged, avatarChanged: false, avatarNeedsDownload: false };
1585+
}
14941586

1495-
const originalAvatar = this.getAvatarInProfilePath();
1496-
const originalFallbackAvatar = this.getFallbackAvatarInProfilePath();
1587+
this.set({
1588+
avatarPointer: newProfile.avatarPointer,
1589+
avatarInProfile: newProfile.avatarPath,
1590+
fallbackAvatarInProfile: newProfile.fallbackAvatarPath,
1591+
profileKey: newProfileKeyHex,
1592+
});
14971593

1498-
// if no changes are needed, return early
1499-
if (
1500-
isEqual(originalAvatar, newProfile.avatarPath) &&
1501-
isEqual(originalFallbackAvatar, newProfile.fallbackAvatarPath) &&
1502-
isEqual(existingAvatarPointer, newProfile.avatarPointer) &&
1503-
isEqual(existingProfileKeyHex, newProfileKeyHex)
1504-
) {
1505-
if (changed) {
15061594
await this.commit();
1595+
return { nameChanged, avatarNeedsDownload: false, avatarChanged: true };
15071596
}
1508-
return changed;
1597+
default:
1598+
assertUnreachable(type, `handlePrivateProfileUpdate: unhandled case "${type}"`);
15091599
}
15101600

1511-
this.set({
1512-
avatarPointer: newProfile.avatarPointer,
1513-
avatarInProfile: newProfile.avatarPath,
1514-
fallbackAvatarInProfile: newProfile.fallbackAvatarPath,
1515-
profileKey: newProfileKeyHex,
1516-
});
1517-
1518-
await this.commit();
1519-
return true;
1601+
throw new Error('Should have returned earlier');
15201602
}
15211603

15221604
public getIsTrustedForAttachmentDownload() {
@@ -1576,7 +1658,18 @@ export class ConversationModel extends Model<ConversationAttributes> {
15761658
this.set({ lastMessageInteractionType: null, lastMessageInteractionStatus: null });
15771659
}
15781660

1661+
/**
1662+
* Update the display name of this conversation with the provided one.
1663+
* Note: cannot be called with private chats.
1664+
* Instead use `setSessionProfile`.
1665+
* The reason is that we might have a more recent name set for this user already, and we need to discard the change
1666+
* if what we are about to apply is older than what we have.
1667+
*/
15791668
public setSessionDisplayNameNoCommit(newDisplayName?: string | null) {
1669+
if (this.isPrivate()) {
1670+
// the name change is only allowed through setSessionProfile for private chats now, see above.
1671+
throw new Error('setSessionDisplayNameNoCommit should not be called with private chats');
1672+
}
15801673
const existingSessionName = this.getRealSessionUsername();
15811674
if (newDisplayName !== existingSessionName && newDisplayName) {
15821675
this.set({ displayNameInProfile: newDisplayName });

ts/models/conversationAttributes.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,27 @@ export enum ConvoTypeNarrow {
227227
*/
228228
group = 'group',
229229
}
230+
231+
export type WithAvatarPointer = {
232+
avatarPointer: string;
233+
};
234+
235+
export type WithProfileUpdatedAtSeconds = {
236+
profileUpdatedAtSeconds: number;
237+
};
238+
239+
export type WithProfileKey = {
240+
profileKey: Uint8Array | string;
241+
};
242+
243+
export type WithAvatarPointerProfileKey = WithAvatarPointer & WithProfileKey;
244+
245+
export type WithAvatarPath = {
246+
avatarPath: string;
247+
};
248+
249+
export type WithFallbackAvatarPath = {
250+
fallbackAvatarPath: string;
251+
};
252+
253+
export type WithAvatarPathAndFallback = WithAvatarPath & WithFallbackAvatarPath;

0 commit comments

Comments
 (0)