@@ -101,6 +101,11 @@ import {
101
101
isOpenOrClosedGroup ,
102
102
READ_MESSAGE_STATE ,
103
103
type ConversationNotificationSettingType ,
104
+ type WithAvatarPathAndFallback ,
105
+ type WithAvatarPointer ,
106
+ type WithAvatarPointerProfileKey ,
107
+ type WithProfileKey ,
108
+ type WithProfileUpdatedAtSeconds ,
104
109
} from './conversationAttributes' ;
105
110
106
111
import { ReadReceiptMessage } from '../session/messages/outgoing/controlMessage/receipt/ReadReceiptMessage' ;
@@ -150,12 +155,66 @@ import type {
150
155
} from '../interactions/types' ;
151
156
import type { LastMessageStatusType } from '../state/ducks/types' ;
152
157
import { OutgoingUserProfile } from '../types/message' ;
158
+ import { Timestamp } from '../types/timestamp/timestamp' ;
153
159
154
160
type InMemoryConvoInfos = {
155
161
mentionedUs : boolean ;
156
162
unreadCount : number ;
157
163
} ;
158
164
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
+
159
218
/**
160
219
* Some fields are not stored in the database, but are kept in memory.
161
220
* 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> {
1406
1465
}
1407
1466
}
1408
1467
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
+
1409
1477
/**
1410
1478
* Updates this conversation with the provided displayName, avatarPath, staticAvatarPath and avatarPointer.
1411
1479
* - displayName can be set to null to not update the display name.
1412
1480
* - if any of the avatar fields is set, they all need to be set.
1413
1481
*
1414
1482
* This function does commit to the DB if any changes are detected.
1415
1483
*/
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 ;
1436
1490
1437
1491
const existingSessionName = this . getRealSessionUsername ( ) ;
1438
1492
if ( newProfile . displayName !== existingSessionName && newProfile . displayName ) {
1439
1493
this . set ( {
1440
1494
displayNameInProfile : newProfile . displayName ,
1441
1495
} ) ;
1442
- changed = true ;
1496
+ nameChanged = true ;
1443
1497
}
1498
+ const type = newProfile . type ;
1444
1499
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 } ;
1462
1506
}
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 ) {
1480
1532
await this . commit ( ) ;
1481
1533
}
1482
- return changed ;
1534
+ return { nameChanged , avatarNeedsDownload : false , avatarChanged } ;
1483
1535
}
1484
- this . set ( { avatarPointer : newProfile . avatarPointer , profileKey : newProfileKeyHex } ) ;
1485
1536
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 } ) ;
1487
1557
1488
- return true ;
1489
- }
1558
+ await this . commit ( ) ;
1490
1559
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
+ }
1494
1586
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
+ } ) ;
1497
1593
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 ) {
1506
1594
await this . commit ( ) ;
1595
+ return { nameChanged, avatarNeedsDownload : false , avatarChanged : true } ;
1507
1596
}
1508
- return changed ;
1597
+ default :
1598
+ assertUnreachable ( type , `handlePrivateProfileUpdate: unhandled case "${ type } "` ) ;
1509
1599
}
1510
1600
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' ) ;
1520
1602
}
1521
1603
1522
1604
public getIsTrustedForAttachmentDownload ( ) {
@@ -1576,7 +1658,18 @@ export class ConversationModel extends Model<ConversationAttributes> {
1576
1658
this . set ( { lastMessageInteractionType : null , lastMessageInteractionStatus : null } ) ;
1577
1659
}
1578
1660
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
+ */
1579
1668
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
+ }
1580
1673
const existingSessionName = this . getRealSessionUsername ( ) ;
1581
1674
if ( newDisplayName !== existingSessionName && newDisplayName ) {
1582
1675
this . set ( { displayNameInProfile : newDisplayName } ) ;
0 commit comments