From 13da2b5632b94c97df4f2609b192755931d70765 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 24 Jan 2024 10:42:26 +1100 Subject: [PATCH] fix: add way to render group update add with history --- _locales/en/messages.json | 22 ++++--- protos/SignalService.proto | 4 +- .../conversation/TimerNotification.tsx | 4 +- .../message-item/GroupUpdateMessage.tsx | 16 ++++-- ts/models/message.ts | 10 +++- ts/models/messageType.ts | 1 + ts/receiver/closedGroups.ts | 1 + ts/receiver/groupv2/handleGroupV2Message.ts | 2 +- ts/session/group/closed-group.ts | 10 ++-- .../GroupUpdateMemberChangeMessage.ts | 38 ++++++++++--- ts/state/ducks/conversations.ts | 1 + ts/state/ducks/metaGroups.ts | 57 ++++++++++++------- ts/state/selectors/selectedConversation.ts | 7 +-- ts/types/LocalizerKeys.ts | 9 ++- 14 files changed, 121 insertions(+), 61 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cb1f00a66..53145bd93 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -233,11 +233,12 @@ "disappearingMessagesModeLegacy": "Legacy", "disappearingMessagesModeLegacySubtitle": "Original version of disappearing messages.", "disappearingMessagesDisabled": "Disappearing messages disabled", - "disabledDisappearingMessages": "$name$ has turned off disappearing messages.", - "youDisabledDisappearingMessages": "You have turned off disappearing messages.", - "youDisabledYourDisappearingMessages": "You turned off disappearing messages. Messages you send will no longer disappear.", + "groupDisabledDisappearingMessages": "$name$ has turned disappearing messages off.", + "updateDisappearingMessagesFallback": "$name$ updated disappearing message settings.", + "youDisabledDisappearingMessages": "You have turned disappearing messages off.", + "youDisabledYourDisappearingMessages": "You turned disappearing messages off. Messages you send will no longer disappear.", "youSetYourDisappearingMessages": "You set your messages to disappear $time$ after they have been $type$.", - "theyDisabledTheirDisappearingMessages": "$name$ has turned off disappearing messages. Messages they send will no longer disappear.", + "theyDisabledTheirDisappearingMessages": "$name$ has turned disappearing messages off. Messages they send will no longer disappear.", "theySetTheirDisappearingMessages": "$name$ has set their messages to disappear $time$ after they have been $type$.", "timerSetTo": "Disappearing message time set to $time$", "set": "Set", @@ -271,10 +272,15 @@ "groupNameChangeFallback": "Group name updated.", "groupAvatarChange": "Group display picture updated.", - "groupOneJoined": "$name$ joined the group.", - "groupYouJoined": "You joined the group.", - "groupTwoJoined": "$first$ and $second$ joined the group.", - "groupOthersJoined": "$name$ and $count$ others joined the group.", + "groupOneJoined": "$name$ was invited to join the group.", + "groupYouJoined": "You were invited to join the group.", + "groupTwoJoined": "$first$ and $second$ were invited to join the group.", + "groupOthersJoined": "$name$ and $count$ others were invited to join the group.", + + "groupOneJoinedWithHistory": "$name$ was invited to join the group. Chat history was shared.", + "groupYouJoinedWithHistory": "You were invited to join the group. Chat history was shared.", + "groupTwoJoinedWithHistory": "$first$ and $second$ were invited to join the group. Chat history was shared.", + "groupOthersJoinedWithHistory": "$name$ and $count$ others were invited to join the group. Chat history was shared.", "groupOneRemoved": "$name$ was removed from the group.", "groupYouRemoved": "You were removed from the group.", diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 04237ac4f..06da60761 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -112,7 +112,9 @@ message GroupUpdateMemberChangeMessage { required Type type = 1; repeated string memberSessionIds = 2; - required bytes adminSignature = 3; + optional bool historyShared = 3; + required bytes adminSignature = 4; + } diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index d556d5afc..975530b46 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -150,7 +150,9 @@ function useTextToRender(props: PropsForExpirationTimer) { case 'fromOther': return disabled ? window.i18n( - ownSideOnly ? 'theyDisabledTheirDisappearingMessages' : 'disabledDisappearingMessages', + ownSideOnly + ? 'theyDisabledTheirDisappearingMessages' + : 'groupDisabledDisappearingMessages', [contact, timespanText] ) : mode diff --git a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx index 359d196d4..507ae6a75 100644 --- a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx +++ b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx @@ -57,7 +57,7 @@ function changeOfMembersV2({ type, us, }: { - type: 'added' | 'promoted' | 'removed'; + type: 'added' | 'addedWithHistory' | 'promoted' | 'removed'; changedWithNames: Array; us: PubkeyType; }): string { @@ -75,7 +75,13 @@ function changeOfMembersV2({ : 'Others'; const action = - type === 'added' ? 'Joined' : type === 'promoted' ? 'Promoted' : ('Removed' as const); + type === 'addedWithHistory' + ? 'JoinedWithHistory' + : type === 'added' + ? 'Joined' + : type === 'promoted' + ? 'Promoted' + : ('Removed' as const); const key = `group${subject}${action}` as const; return window.i18n( @@ -85,7 +91,7 @@ function changeOfMembersV2({ } // TODO those lookups might need to be memoized -const ChangeItemJoined = (added: Array): string => { +const ChangeItemJoined = (added: Array, withHistory: boolean): string => { if (!added.length) { throw new Error('Group update add is missing contacts'); } @@ -95,7 +101,7 @@ const ChangeItemJoined = (added: Array): string => { if (isGroupV2) { return changeOfMembersV2({ changedWithNames: mapIdsWithNames(added, names), - type: 'added', + type: withHistory ? 'addedWithHistory' : 'added', us, }); } @@ -182,7 +188,7 @@ const ChangeItem = (change: PropsForGroupUpdateType): string => { case 'name': return ChangeItemName(change.newName); case 'add': - return ChangeItemJoined(change.added); + return ChangeItemJoined(change.added, change.withHistory); case 'left': return ChangeItemLeft(change.left); case 'kicked': diff --git a/ts/models/message.ts b/ts/models/message.ts index 9c78a0314..65a2cb937 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -455,6 +455,15 @@ export class MessageModel extends Backbone.Model { const change: PropsForGroupUpdateAdd = { type: 'add', added: groupUpdate.joined as Array, + withHistory: false, + }; + return { change, ...sharedProps }; + } + if (groupUpdate.joinedWithHistory?.length) { + const change: PropsForGroupUpdateAdd = { + type: 'add', + added: groupUpdate.joined as Array, + withHistory: true, }; return { change, ...sharedProps }; } @@ -522,7 +531,6 @@ export class MessageModel extends Backbone.Model { return undefined; } - const readBy = this.get('read_by') || []; if (Storage.get(SettingsKey.settingsReadReceipt) && readBy.length > 0) { return 'read'; diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index f851afc0e..7361468b5 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -156,6 +156,7 @@ export type PropsForMessageRequestResponse = MessageRequestResponseMsg & { export type MessageGroupUpdate = { left?: Array; joined?: Array; + joinedWithHistory?: Array; kicked?: Array; promoted?: Array; name?: string; diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 3c417a299..0ae1bd6d6 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -689,6 +689,7 @@ async function handleClosedGroupMembersAdded( const groupDiff: GroupDiff = { type: 'add', added: membersNotAlreadyPresent, + withHistory: false, }; await ClosedGroup.addUpdateMessage({ convo, diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts index b84767a73..7a6db1f06 100644 --- a/ts/receiver/groupv2/handleGroupV2Message.ts +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -285,7 +285,7 @@ async function handleGroupMemberChangeMessage({ switch (change.type) { case SignalService.GroupUpdateMemberChangeMessage.Type.ADDED: { await ClosedGroup.addUpdateMessage({ - diff: { type: 'add', added: filteredMemberChange }, + diff: { type: 'add', added: filteredMemberChange, withHistory: change.historyShared }, ...sharedDetails, }); diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index 0d01617c0..0f72a232f 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -89,11 +89,11 @@ async function initiateClosedGroupUpdate( expireTimer, }; - const diff = buildGroupDiff(convo, groupDetails); + const diff = buildGroupV1Diff(convo, groupDetails); await updateOrCreateClosedGroup(groupDetails); if (!diff) { - window.log.warn('buildGroupDiff returned null'); + window.log.warn('buildGroupV1Diff returned null'); await convo.commit(); return; @@ -126,7 +126,7 @@ async function initiateClosedGroupUpdate( } if (diff.type === 'add' && diff.added?.length) { - const joiningOnlyDiff: GroupDiff = _.pick(diff, ['type', 'added']); + const joiningOnlyDiff: GroupDiff = _.pick(diff, ['type', 'added', 'withHistory']); const dbMessageAdded = await addUpdateMessage({ diff: joiningOnlyDiff, @@ -213,7 +213,7 @@ export async function addUpdateMessage({ }); } -function buildGroupDiff(convo: ConversationModel, update: GroupInfo): GroupDiff | null { +function buildGroupV1Diff(convo: ConversationModel, update: GroupInfo): GroupDiff | null { if (convo.getRealSessionUsername() !== update.name) { return { type: 'name', newName: update.name }; } @@ -228,7 +228,7 @@ function buildGroupDiff(convo: ConversationModel, update: GroupInfo): GroupDiff PubKey.is05Pubkey ); if (added.length > 0) { - return { type: 'add', added }; + return { type: 'add', added, withHistory: false }; } // Check if anyone got kicked: const removedMembers = _.difference(oldMembersWithZombies, newMembersWithZombiesLeft).filter( diff --git a/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts b/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts index 0def77978..dbb4f3370 100644 --- a/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts @@ -12,17 +12,22 @@ import { } from '../GroupUpdateMessage'; type MembersAddedMessageParams = GroupUpdateMessageParams & { - typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED; + typeOfChange: 'added'; + added: Array; +}; + +type MembersAddedWithHistoryMessageParams = GroupUpdateMessageParams & { + typeOfChange: 'addedWithHistory'; added: Array; }; type MembersRemovedMessageParams = GroupUpdateMessageParams & { - typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED; + typeOfChange: 'removed'; removed: Array; }; type MembersPromotedMessageParams = GroupUpdateMessageParams & { - typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.PROMOTED; + typeOfChange: 'promoted'; promoted: Array; }; @@ -30,7 +35,8 @@ type MembersPromotedMessageParams = GroupUpdateMessageParams & { * GroupUpdateInfoChangeMessage is sent to the group's swarm. */ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage { - public readonly typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type; + public readonly typeOfChange: 'added' | 'addedWithHistory' | 'removed' | 'promoted'; + public readonly memberSessionIds: Array = []; // added, removed, promoted based on the type. public readonly namespace = SnodeNamespaces.ClosedGroupMessages; private readonly secretKey: Uint8Array; // not sent, only used for signing content as part of the message @@ -41,11 +47,11 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage { | MembersAddedMessageParams | MembersRemovedMessageParams | MembersPromotedMessageParams + | MembersAddedWithHistoryMessageParams ) & AdminSigDetails ) { super(params); - const { Type } = SignalService.GroupUpdateMemberChangeMessage; const { typeOfChange } = params; this.typeOfChange = typeOfChange; @@ -53,21 +59,28 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage { this.sodium = params.sodium; switch (typeOfChange) { - case Type.ADDED: { + case 'added': { if (isEmpty(params.added)) { throw new Error('added members list cannot be empty'); } this.memberSessionIds = params.added; break; } - case Type.REMOVED: { + case 'addedWithHistory': { + if (isEmpty(params.added)) { + throw new Error('addedWithHistory members list cannot be empty'); + } + this.memberSessionIds = params.added; + break; + } + case 'removed': { if (isEmpty(params.removed)) { throw new Error('removed members list cannot be empty'); } this.memberSessionIds = params.removed; break; } - case Type.PROMOTED: { + case 'promoted': { if (isEmpty(params.promoted)) { throw new Error('promoted members list cannot be empty'); } @@ -80,8 +93,15 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage { } public dataProto(): SignalService.DataMessage { + const { Type } = SignalService.GroupUpdateMemberChangeMessage; + const memberChangeMessage = new SignalService.GroupUpdateMemberChangeMessage({ - type: this.typeOfChange, + type: + this.typeOfChange === 'added' || this.typeOfChange === 'addedWithHistory' + ? Type.ADDED + : this.typeOfChange === 'removed' + ? Type.REMOVED + : Type.PROMOTED, memberSessionIds: this.memberSessionIds, adminSignature: this.sodium.crypto_sign_detached( stringToUint8Array(`MEMBER_CHANGE${this.typeOfChange}${this.createAtNetworkTimestamp}`), diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 209041ada..fa40a296c 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -104,6 +104,7 @@ export type PropsForExpirationTimer = { export type PropsForGroupUpdateAdd = { type: 'add'; + withHistory: boolean; added: Array; }; diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 403f1fde7..7d5e0947c 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -608,8 +608,10 @@ async function getUpdateMessagesToPush({ removed, adminSecretKey, createAtNetworkTimestamp, + fromMemberLeftMessage, }: WithAddWithHistoryMembers & WithAddWithoutHistoryMembers & + WithFromMemberLeftMessage & WithRemoveMembers & WithFromCurrentDevice & WithGroupPubkey & { @@ -621,17 +623,29 @@ async function getUpdateMessagesToPush({ const updateMessages: Array = []; - if (!fromCurrentDevice) { + if (!fromCurrentDevice || fromMemberLeftMessage) { return updateMessages; } - const allAdded = [...withHistory, ...withoutHistory]; // those are already enforced to be unique (and without intersection) in `validateMemberChange()` - if (allAdded.length) { + if (withoutHistory.length) { + updateMessages.push( + new GroupUpdateMemberChangeMessage({ + added: withoutHistory, + groupPk, + typeOfChange: 'added', + createAtNetworkTimestamp, + secretKey: adminSecretKey, + sodium, + ...getConvoExpireDetailsForMsg(convo), + }) + ); + } + if (withHistory.length) { updateMessages.push( new GroupUpdateMemberChangeMessage({ - added: allAdded, + added: withHistory, groupPk, - typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED, + typeOfChange: 'addedWithHistory', createAtNetworkTimestamp, secretKey: adminSecretKey, sodium, @@ -644,7 +658,7 @@ async function getUpdateMessagesToPush({ new GroupUpdateMemberChangeMessage({ removed, groupPk, - typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED, + typeOfChange: 'removed', createAtNetworkTimestamp, secretKey: adminSecretKey, sodium, @@ -652,6 +666,7 @@ async function getUpdateMessagesToPush({ }) ); } + // TODO might need to add the promote case here return updateMessages; } @@ -704,6 +719,7 @@ async function handleMemberAddedFromUIOrNot({ withHistory, withoutHistory, createAtNetworkTimestamp, + fromMemberLeftMessage: false, }); await LibSessionUtil.saveDumpsToDb(groupPk); @@ -744,23 +760,21 @@ async function handleMemberAddedFromUIOrNot({ : null, }, }; - const additions = updateMessages.find( - m => m.typeOfChange === SignalService.GroupUpdateMemberChangeMessage.Type.ADDED - ); - if (additions) { + const additionsWithoutHistory = updateMessages.find(m => m.typeOfChange === 'added'); + const additionsWithHistory = updateMessages.find(m => m.typeOfChange === 'addedWithHistory'); + if (additionsWithoutHistory) { await ClosedGroup.addUpdateMessage({ - diff: { type: 'add', added: additions.memberSessionIds }, + diff: { type: 'add', added: additionsWithoutHistory.memberSessionIds, withHistory: false }, ...shared, - expireUpdate: { - expirationTimer: expiringDetails.expireTimer, - expirationType: expiringDetails.expirationType, - messageExpirationFromRetrieve: - expiringDetails.expireTimer > 0 - ? createAtNetworkTimestamp + expiringDetails.expireTimer - : null, - }, }); } + if (additionsWithHistory) { + await ClosedGroup.addUpdateMessage({ + diff: { type: 'add', added: additionsWithHistory.memberSessionIds, withHistory: true }, + ...shared, + }); + } + await convo.commit(); } @@ -819,6 +833,7 @@ async function handleMemberRemovedFromUIOrNot({ withHistory: [], withoutHistory: [], createAtNetworkTimestamp, + fromMemberLeftMessage, }); await LibSessionUtil.saveDumpsToDb(groupPk); @@ -858,9 +873,7 @@ async function handleMemberRemovedFromUIOrNot({ }, }; - const removals = updateMessages.find( - m => m.typeOfChange === SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED - ); + const removals = updateMessages.find(m => m.typeOfChange === 'removed'); if (removals) { await ClosedGroup.addUpdateMessage({ diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index 18e8832fe..2b37eb784 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -87,12 +87,7 @@ export function getSelectedCanWrite(state: StateType) { const isBlindedAndDisabledMsgRequests = getSelectedBlindedDisabledMsgRequests(state); // true if isPrivate, blinded and explicitely disabled msgreq - return !( - isBlocked || - isKickedFromGroup || - readOnlySogs || - isBlindedAndDisabledMsgRequests - ); + return !(isBlocked || isKickedFromGroup || readOnlySogs || isBlindedAndDisabledMsgRequests); } function getSelectedBlindedDisabledMsgRequests(state: StateType) { diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 6fc59115e..61e390416 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -142,7 +142,6 @@ export type LocalizerKeys = | 'dialogClearAllDataDeletionFailedTitle' | 'dialogClearAllDataDeletionFailedTitleQuestion' | 'dialogClearAllDataDeletionQuestion' - | 'disabledDisappearingMessages' | 'disappearingMessages' | 'disappearingMessagesDisabled' | 'disappearingMessagesModeAfterRead' @@ -206,6 +205,7 @@ export type LocalizerKeys = | 'goToReleaseNotes' | 'goToSupportPage' | 'groupAvatarChange' + | 'groupDisabledDisappearingMessages' | 'groupInviteFailedOne' | 'groupInviteFailedOthers' | 'groupInviteFailedTwo' @@ -214,16 +214,20 @@ export type LocalizerKeys = | 'groupNameChangeFallback' | 'groupNamePlaceholder' | 'groupOneJoined' + | 'groupOneJoinedWithHistory' | 'groupOneLeft' | 'groupOnePromoted' | 'groupOneRemoved' | 'groupOthersJoined' + | 'groupOthersJoinedWithHistory' | 'groupOthersPromoted' | 'groupOthersRemoved' | 'groupTwoJoined' + | 'groupTwoJoinedWithHistory' | 'groupTwoPromoted' | 'groupTwoRemoved' | 'groupYouJoined' + | 'groupYouJoinedWithHistory' | 'groupYouLeft' | 'groupYouPromoted' | 'groupYouRemoved' @@ -534,6 +538,7 @@ export type LocalizerKeys = | 'unknownCountry' | 'unpinConversation' | 'unreadMessages' + | 'updateDisappearingMessagesFallback' | 'updateGroupDialogTitle' | 'userAddedToModerators' | 'userBanFailed' @@ -563,8 +568,8 @@ export type LocalizerKeys = | 'youGotKickedFromGroup' | 'youHaveANewFriendRequest' | 'youLeftTheGroup' - | 'youWereInvitedToGroup' | 'youSetYourDisappearingMessages' + | 'youWereInvitedToGroup' | 'yourSessionID' | 'yourUniqueSessionID' | 'zoomFactorSettingTitle';