From eb0ddd85f4477e2441ea8fcda0936241c5bc91e2 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 5 May 2021 13:09:01 +1000 Subject: [PATCH] add zombies logic for member leaving and removing --- _locales/en/messages.json | 4 + js/views/invite_contacts_dialog_view.js | 7 +- js/views/update_group_dialog_view.js | 88 +++++++------ stylesheets/_session.scss | 4 + .../conversation/InviteContactsDialog.tsx | 13 +- .../conversation/UpdateGroupMembersDialog.tsx | 122 +++++++++++++++--- .../session/SessionMemberListItem.tsx | 13 +- .../conversation/SessionRightPanel.tsx | 2 +- ts/models/conversation.ts | 4 + ts/opengroup/opengroupV2/OpenGroupAPIV2.ts | 4 + ts/receiver/closedGroups.ts | 36 +++--- ts/session/group/index.ts | 33 +++-- ts/session/utils/Toast.tsx | 8 ++ ts/test/test-utils/utils/message.ts | 1 + 14 files changed, 245 insertions(+), 94 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 58d6d110e..7e99cf744 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2015,6 +2015,10 @@ "noModeratorsToRemove": { "message": "no moderators to remove" }, + "onlyAdminCanRemoveMembers": { "message": "You are not the creator" }, + "onlyAdminCanRemoveMembersDesc": { + "message": "Only the creator of the group can remove users" + }, "createAccount": { "message": "Create Account" }, diff --git a/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index db57a61b9..e00173507 100644 --- a/js/views/invite_contacts_dialog_view.js +++ b/js/views/invite_contacts_dialog_view.js @@ -17,8 +17,13 @@ d => !!d && !d.isBlocked() && d.isPrivate() && !d.isMe() && !!d.get('active_at') ); if (!convo.isPublic()) { + // filter our zombies and current members from the list of contact we can add + const members = convo.get('members') || []; - this.contacts = this.contacts.filter(d => !members.includes(d.id)); + const zombies = convo.get('zombies') || []; + this.contacts = this.contacts.filter( + d => !members.includes(d.id) && !zombies.includes(d.id) + ); } this.chatName = convo.get('name'); diff --git a/js/views/update_group_dialog_view.js b/js/views/update_group_dialog_view.js index 74e3d461d..15e4486da 100644 --- a/js/views/update_group_dialog_view.js +++ b/js/views/update_group_dialog_view.js @@ -87,37 +87,29 @@ this.theme = groupConvo.theme; if (this.isPublic) { - this.titleText = i18n('updateGroupDialogTitle', this.groupName); - // I'd much prefer to integrate mods with groupAdmins - // but lets discuss first... - this.isAdmin = groupConvo.isAdmin(window.storage.get('primaryDevicePubKey')); - // zero out contactList for now - this.contactsAndMembers = []; - this.existingMembers = []; - } else { - this.titleText = i18n('updateGroupDialogTitle', this.groupName); - // anybody can edit a closed group name or members - const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - this.isAdmin = groupConvo.isMediumGroup() - ? true - : groupConvo.get('groupAdmins').includes(ourPK); - this.admins = groupConvo.get('groupAdmins'); - const convos = window - .getConversationController() - .getConversations() - .filter(d => !!d); - - this.existingMembers = groupConvo.get('members') || []; - // Show a contact if they are our friend or if they are a member - this.contactsAndMembers = convos.filter( - d => this.existingMembers.includes(d.id) && d.isPrivate() && !d.isMe() - ); - this.contactsAndMembers = _.uniq(this.contactsAndMembers, true, d => d.id); + throw new Error('UpdateGroupMembersDialog is only made for Closed/Medium groups'); + } + this.titleText = i18n('updateGroupDialogTitle', this.groupName); + // anybody can edit a closed group name or members + const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); + this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK); + this.admins = groupConvo.get('groupAdmins'); + const convos = window + .getConversationController() + .getConversations() + .filter(d => !!d); + + this.existingMembers = groupConvo.get('members') || []; + this.existingZombies = groupConvo.get('zombies') || []; + // Show a contact if they are our friend or if they are a member + this.contactsAndMembers = convos.filter( + d => this.existingMembers.includes(d.id) && d.isPrivate() && !d.isMe() + ); + this.contactsAndMembers = _.uniq(this.contactsAndMembers, true, d => d.id); - // at least make sure it's an array - if (!Array.isArray(this.existingMembers)) { - this.existingMembers = []; - } + // at least make sure it's an array + if (!Array.isArray(this.existingMembers)) { + this.existingMembers = []; } this.$el.focus(); @@ -133,6 +125,7 @@ cancelText: i18n('cancel'), isPublic: this.isPublic, existingMembers: this.existingMembers, + existingZombies: this.existingZombies, contactList: this.contactsAndMembers, isAdmin: this.isAdmin, admins: this.admins, @@ -149,15 +142,32 @@ async onSubmit(newMembers) { const _ = window.Lodash; const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - const allMembers = window.Lodash.concat(newMembers, [ourPK]); + + const allMembersAfterUpdate = window.Lodash.concat(newMembers, [ourPK]); + + if (!this.isAdmin) { + window.log.warn('Skipping update of members, we are not the admin'); + return; + } + // new members won't include the zombies. We are the admin and we want to remove them not matter what // We need to NOT trigger an group update if the list of member is the same. - const notPresentInOld = allMembers.filter(m => !this.existingMembers.includes(m)); + // we need to merge all members, including zombies for this call. - const membersToRemove = this.existingMembers.filter(m => !allMembers.includes(m)); + // we consider that the admin ALWAYS wants to remove zombies (actually they should be removed + // automatically by him when the LEFT message is received) + const allExistingMembersWithZombies = _.uniq( + this.existingMembers.concat(this.existingZombies) + ); - // If any extra devices of removed exist in newMembers, ensure that you filter them - const filteredMemberes = allMembers.filter(member => !_.includes(membersToRemove, member)); + const notPresentInOld = allMembersAfterUpdate.filter( + m => !allExistingMembersWithZombies.includes(m) + ); + + // be sure to include zombies in here + const membersToRemove = allExistingMembersWithZombies.filter( + m => !allMembersAfterUpdate.includes(m) + ); const xor = _.xor(membersToRemove, notPresentInOld); if (xor.length === 0) { @@ -166,10 +176,16 @@ return; } + // If any extra devices of removed exist in newMembers, ensure that you filter them + // Note: I think this is useless + const filteredMembers = allMembersAfterUpdate.filter( + member => !_.includes(membersToRemove, member) + ); + window.libsession.ClosedGroup.initiateGroupUpdate( this.groupId, this.groupName, - filteredMemberes, + filteredMembers, this.avatarPath ); }, diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index d3946e819..303f0a013 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1300,6 +1300,10 @@ input { } } + &.zombie { + opacity: 0.5; + } + &__checkmark { opacity: 0; transition: $session-transition-duration; diff --git a/ts/components/conversation/InviteContactsDialog.tsx b/ts/components/conversation/InviteContactsDialog.tsx index 11ca09e2d..228fa7902 100644 --- a/ts/components/conversation/InviteContactsDialog.tsx +++ b/ts/components/conversation/InviteContactsDialog.tsx @@ -10,6 +10,7 @@ import { initiateGroupUpdate } from '../../session/group'; import { ConversationModel, ConversationTypeEnum } from '../../models/conversation'; import { getCompleteUrlForV2ConvoId } from '../../interactions/conversation'; import _ from 'lodash'; +import autoBind from 'auto-bind'; interface Props { contactList: Array; chatName: string; @@ -26,12 +27,7 @@ class InviteContactsDialogInner extends React.Component { constructor(props: any) { super(props); - this.onMemberClicked = this.onMemberClicked.bind(this); - this.closeDialog = this.closeDialog.bind(this); - this.onClickOK = this.onClickOK.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.submitForOpenGroup = this.submitForOpenGroup.bind(this); - this.submitForClosedGroup = this.submitForClosedGroup.bind(this); + autoBind(this); let contacts = this.props.contactList; @@ -137,11 +133,10 @@ class InviteContactsDialogInner extends React.Component { private async submitForClosedGroup(pubkeys: Array) { const { convo } = this.props; - // FIXME audric is this dialog still used for closed groups? I think - // public group chats - // private group chats + // closed group chats const ourPK = UserUtils.getOurPubKeyStrFromCache(); + // we only care about real members. If a member is currently a zombie we have to be able to add him back let existingMembers = convo.get('members') || []; // at least make sure it's an array if (!Array.isArray(existingMembers)) { diff --git a/ts/components/conversation/UpdateGroupMembersDialog.tsx b/ts/components/conversation/UpdateGroupMembersDialog.tsx index 52c8f1678..99b88d48b 100644 --- a/ts/components/conversation/UpdateGroupMembersDialog.tsx +++ b/ts/components/conversation/UpdateGroupMembersDialog.tsx @@ -6,6 +6,11 @@ import { SessionButton, SessionButtonColor } from '../session/SessionButton'; import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem'; import { DefaultTheme } from 'styled-components'; import { ToastUtils } from '../../session/utils'; +import { LocalizerType } from '../../types/Util'; +import autoBind from 'auto-bind'; +import { ConversationController } from '../../session/conversations'; + +import _ from 'lodash'; interface Props { titleText: string; @@ -15,10 +20,11 @@ interface Props { // contacts not in the group contactList: Array; isAdmin: boolean; - existingMembers: Array; - admins: Array; // used for closed group + existingMembers: Array; + existingZombies: Array; + admins: Array; // used for closed group - i18n: any; + i18n: LocalizerType; onSubmit: any; onClose: any; theme: DefaultTheme; @@ -26,23 +32,21 @@ interface Props { interface State { contactList: Array; + zombies: Array; errorDisplayed: boolean; errorMessage: string; } export class UpdateGroupMembersDialog extends React.Component { - constructor(props: any) { + constructor(props: Props) { super(props); - this.onMemberClicked = this.onMemberClicked.bind(this); - this.onClickOK = this.onClickOK.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.closeDialog = this.closeDialog.bind(this); + autoBind(this); let contacts = this.props.contactList; contacts = contacts.map(d => { const lokiProfile = d.getLokiProfile(); - const name = lokiProfile ? lokiProfile.displayName : 'Anonymous'; + const name = lokiProfile ? lokiProfile.displayName : window.i18n('anonymous'); const existingMember = this.props.existingMembers.includes(d.id); @@ -58,8 +62,33 @@ export class UpdateGroupMembersDialog extends React.Component { }; }); + const zombies = _.compact( + this.props.existingZombies.map(d => { + const convo = ConversationController.getInstance().get(d); + if (!convo) { + window.log.warn('Zombie convo not found'); + return null; + } + const lokiProfile = convo.getLokiProfile(); + const name = lokiProfile ? `${lokiProfile.displayName} (Zombie)` : window.i18n('anonymous'); + + const existingZombie = this.props.existingZombies.includes(convo.id); + return { + id: convo.id, + authorPhoneNumber: convo.id, + authorProfileName: name, + authorAvatarPath: convo?.getAvatarPath() as string, + selected: false, + authorName: name, + checkmarked: false, + existingMember: existingZombie, + }; + }) + ); + this.state = { contactList: contacts, + zombies, errorDisplayed: false, errorMessage: '', }; @@ -70,13 +99,14 @@ export class UpdateGroupMembersDialog extends React.Component { public onClickOK() { const members = this.getWouldBeMembers(this.state.contactList).map(d => d.id); + // do not include zombies here, they are removed by force this.props.onSubmit(members); this.closeDialog(); } public render() { - const { okText, cancelText, contactList, titleText } = this.props; + const { okText, cancelText, isAdmin, contactList, titleText } = this.props; const showNoMembersMessage = contactList.length === 0; @@ -99,17 +129,20 @@ export class UpdateGroupMembersDialog extends React.Component {
{this.renderMemberList()}
+ {this.renderZombiesList()} {showNoMembersMessage &&

{window.i18n('noMembersInThisGroup')}

}
- + {isAdmin && ( + + )}
); @@ -131,6 +164,21 @@ export class UpdateGroupMembersDialog extends React.Component { )); } + private renderZombiesList() { + return this.state.zombies.map((member: ContactType, index: number) => ( + + )); + } + private onKeyUp(event: any) { switch (event.key) { case 'Enter': @@ -158,10 +206,15 @@ export class UpdateGroupMembersDialog extends React.Component { this.props.onClose(); } - private onMemberClicked(selected: any) { + private onMemberClicked(selected: ContactType) { const { isAdmin, admins } = this.props; const { contactList } = this.state; + if (!isAdmin) { + ToastUtils.pushOnlyAdminCanRemove(); + return; + } + if (selected.existingMember && !isAdmin) { window.log.warn('Only group admin can remove members!'); return; @@ -190,4 +243,41 @@ export class UpdateGroupMembersDialog extends React.Component { }; }); } + + private onZombieClicked(selected: ContactType) { + const { isAdmin, admins } = this.props; + const { zombies } = this.state; + + if (!isAdmin) { + ToastUtils.pushOnlyAdminCanRemove(); + return; + } + if (selected.existingMember && !isAdmin) { + window.log.warn('Only group admin can remove members!'); + return; + } + + if (selected.existingMember && admins.includes(selected.id)) { + window.log.warn( + `User ${selected.id} cannot be removed as they are the creator of the closed group.` + ); + ToastUtils.pushCannotRemoveCreatorFromGroup(); + return; + } + + const updatedZombies = zombies.map(zombie => { + if (zombie.id === selected.id) { + return { ...zombie, checkmarked: !zombie.checkmarked }; + } else { + return zombie; + } + }); + + this.setState(state => { + return { + ...state, + zombies: updatedZombies, + }; + }); + } } diff --git a/ts/components/session/SessionMemberListItem.tsx b/ts/components/session/SessionMemberListItem.tsx index 550511a37..890a29adc 100644 --- a/ts/components/session/SessionMemberListItem.tsx +++ b/ts/components/session/SessionMemberListItem.tsx @@ -6,6 +6,7 @@ import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; import { Constants } from '../../session'; import { DefaultTheme } from 'styled-components'; import { PubKey } from '../../session/types'; +import autoBind from 'auto-bind'; export interface ContactType { id: string; @@ -22,6 +23,8 @@ interface Props { member: ContactType; index: number; // index in the list isSelected: boolean; + // this bool is used to make a zombie appear with less opacity than a normal member + isZombie?: boolean; onSelect?: any; onUnselect?: any; theme: DefaultTheme; @@ -35,14 +38,11 @@ class SessionMemberListItemInner extends React.Component { constructor(props: any) { super(props); - this.handleSelectionAction = this.handleSelectionAction.bind(this); - this.selectMember = this.selectMember.bind(this); - this.unselectMember = this.unselectMember.bind(this); - this.renderAvatar = this.renderAvatar.bind(this); + autoBind(this); } public render() { - const { isSelected, member } = this.props; + const { isSelected, member, isZombie } = this.props; const name = member.authorProfileName || PubKey.shorten(member.authorPhoneNumber); @@ -51,7 +51,8 @@ class SessionMemberListItemInner extends React.Component { className={classNames( `session-member-item-${this.props.index}`, 'session-member-item', - isSelected && 'selected' + isSelected && 'selected', + isZombie && 'zombie' )} onClick={this.handleSelectionAction} role="button" diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index c841a461d..349db3f0c 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -252,7 +252,7 @@ class SessionRightPanel extends React.Component { const showUpdateGroupNameButton = isAdmin && !commonNoShow; const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic; - const showUpdateGroupMembersButton = !isPublic && !commonNoShow && isAdmin; + const showUpdateGroupMembersButton = !isPublic && !commonNoShow; return (
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index b8c9b5be4..ad1965522 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -52,7 +52,9 @@ export interface ConversationAttributes { profileName?: string; id: string; name?: string; + // members are all members for this group. zombies excluded members: Array; + zombies: Array; // only used for closed groups. Zombies are users which left but not yet removed by the admin left: boolean; expireTimer: number; mentionedUs: boolean; @@ -90,6 +92,7 @@ export interface ConversationAttributesOptionals { id: string; name?: string; members?: Array; + zombies?: Array; left?: boolean; expireTimer?: number; mentionedUs?: boolean; @@ -130,6 +133,7 @@ export const fillConvoAttributesWithDefaults = ( ): ConversationAttributes => { return _.defaults(optAttributes, { members: [], + zombies: [], left: false, unreadCount: 0, lastMessageStatus: null, diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts index a5b5db48e..261707db0 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts @@ -451,6 +451,10 @@ export const downloadFileOpenGroupV2 = async ( fileId: number, roomInfos: OpenGroupRequestCommonType ): Promise => { + if (!fileId) { + window.log.warn('downloadFileOpenGroupV2: FileId cannot be unset. returning null'); + return null; + } const request: OpenGroupV2Request = { method: 'GET', room: roomInfos.roomId, diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 6b12bbc9f..5aae9ac61 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -584,6 +584,9 @@ async function handleClosedGroupMembersAdded( await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); convo.set({ members }); + // make sure those members are not on our zombie list + addedMembers.forEach(added => removeMemberFromZombies(envelope, PubKey.cast(added), convo)); + convo.updateLastMessage(); await convo.commit(); await removeFromCache(envelope); @@ -633,16 +636,9 @@ async function handleClosedGroupMembersRemoved( const ourPubKey = UserUtils.getOurPubKeyFromCache(); const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); if (wasCurrentUserRemoved) { - await markGroupAsLeftOrKicked(groupPublicKey, convo, true); - } - // Generate and distribute a new encryption key pair if needed - if (await areWeAdmin(convo)) { - try { - await ClosedGroup.generateAndSendNewEncryptionKeyPair(groupPubKey, membersAfterUpdate); - } catch (e) { - window.log.warn('Could not distribute new encryption keypair.'); - } + await markGroupAsLeftOrKicked(groupPubKey, convo, true); } + // Note: we don't want to send a new encryption keypair when we get a member removed. // Only add update message if we have something to show if (membersAfterUpdate.length !== currentMembers.length) { @@ -660,6 +656,8 @@ async function handleClosedGroupMembersRemoved( // Update the group convo.set({ members: membersAfterUpdate }); + const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z)); + convo.set({ zombies }); await convo.commit(); await removeFromCache(envelope); @@ -762,6 +760,7 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver // otherwise, we remove the sender from the list of current members in this group const oldMembers = convo.get('members') || []; const newMembers = oldMembers.filter(s => s !== sender); + window.log.info(`Got a group update for group ${envelope.source}, type: MEMBER_LEFT`); // Show log if we sent this message ourself (from another device or not) if (UserUtils.isUsFromCache(sender)) { @@ -784,12 +783,6 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver return; } - // if we are the admin, and there are still some members after the member left, we send a new keypair - // to the remaining members - if (isCurrentUserAdmin && !!newMembers.length) { - await ClosedGroup.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers); - } - // Another member left, not us, not the admin, just another member. // But this member was in the list of members (as performIfValid checks for that) const groupDiff: ClosedGroup.GroupDiff = { @@ -797,8 +790,19 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver }; await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); convo.updateLastMessage(); - await addMemberToZombies(envelope, PubKey.cast(sender), convo); + // if a user just left and we are the admin, we remove him right away for everyone by sending a members removed + if (!isCurrentUserAdmin && oldMembers.includes(sender)) { + addMemberToZombies(envelope, PubKey.cast(sender), convo); + } convo.set('members', newMembers); + + // if we are the admin, and there are still some members after the member left, we send a new keypair + // to the remaining members. + // also if we are the admin, we can tell to everyone that this user is effectively removed + if (isCurrentUserAdmin && !!newMembers.length) { + await ClosedGroup.sendRemovedMembers(convo, [sender], newMembers); + } + await convo.commit(); await removeFromCache(envelope); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 0d333f604..44dcb7ed9 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -40,7 +40,8 @@ import { updateOpenGroupV2 } from '../../opengroup/opengroupV2/OpenGroupUpdate'; export interface GroupInfo { id: string; name: string; - members: Array; // Primary keys + members: Array; + zombies?: Array; active?: boolean; expireTimer?: number | null; avatar?: any; @@ -107,6 +108,8 @@ export async function initiateGroupUpdate( if (!isMediumGroup) { throw new Error('Legacy group are not supported anymore.'); } + const oldZombies = convo.get('zombies'); + console.warn('initiategroupUpdate old zombies:', oldZombies); // do not give an admins field here. We don't want to be able to update admins and // updateOrCreateClosedGroup() will update them if given the choice. @@ -114,10 +117,13 @@ export async function initiateGroupUpdate( id: groupId, name: groupName, members, + // remove from the zombies list the zombies not which are not in the group anymore + zombies: convo.get('zombies').filter(z => members.includes(z)), active: true, expireTimer: convo.get('expireTimer'), avatar, }; + console.warn('initiategroupUpdate new zombies:', groupDetails.zombies); const diff = buildGroupDiff(convo, groupDetails); @@ -150,8 +156,9 @@ export async function initiateGroupUpdate( const dbMessageLeaving = await addUpdateMessage(convo, leavingOnlyDiff, 'outgoing', Date.now()); MessageController.getInstance().register(dbMessageLeaving.id, dbMessageLeaving); const stillMembers = members; - await sendRemovedMembers(convo, diff.leavingMembers, dbMessageLeaving.id, stillMembers); + await sendRemovedMembers(convo, diff.leavingMembers, stillMembers, dbMessageLeaving.id); } + await convo.commit(); } export async function addUpdateMessage( @@ -253,6 +260,10 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { updates.left = true; } + if (details.zombies) { + updates.zombies = details.zombies; + } + conversation.set(updates); // Update the conversation avatar only if new avatar exists and hash differs @@ -285,10 +296,14 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { if (expireTimer === undefined || typeof expireTimer !== 'number') { return; } - const source = UserUtils.getOurPubKeyStrFromCache(); - await conversation.updateExpirationTimer(expireTimer, source, Date.now(), { - fromSync: true, - }); + await conversation.updateExpirationTimer( + expireTimer, + UserUtils.getOurPubKeyStrFromCache(), + Date.now(), + { + fromSync: true, + } + ); } export async function leaveClosedGroup(groupId: string) { @@ -420,11 +435,11 @@ async function sendAddedMembers( await Promise.all(promises); } -async function sendRemovedMembers( +export async function sendRemovedMembers( convo: ConversationModel, removedMembers: Array, - messageId: string, - stillMembers: Array + stillMembers: Array, + messageId?: string ) { if (!removedMembers?.length) { window.log.warn('No removedMembers given for group update. Skipping'); diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 505d15a2f..ba562a35b 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -183,6 +183,14 @@ export function pushCannotRemoveCreatorFromGroup() { ); } +export function pushOnlyAdminCanRemove() { + pushToastInfo( + 'onlyAdminCanRemoveMembers', + window.i18n('onlyAdminCanRemoveMembers'), + window.i18n('onlyAdminCanRemoveMembersDesc') + ); +} + export function pushUserNeedsToHaveJoined() { pushToastWarning( 'userNeedsToHaveJoined', diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index c20a897db..87b8f0d55 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -81,6 +81,7 @@ export class MockConversation { lastJoinedTimestamp: Date.now(), lastMessageStatus: null, lastMessage: null, + zombies: [], }; }