add zombies logic for member leaving and removing

pull/1592/head
Audric Ackermann 4 years ago
parent 7ec663df71
commit eb0ddd85f4
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -2015,6 +2015,10 @@
"noModeratorsToRemove": { "noModeratorsToRemove": {
"message": "no moderators to remove" "message": "no moderators to remove"
}, },
"onlyAdminCanRemoveMembers": { "message": "You are not the creator" },
"onlyAdminCanRemoveMembersDesc": {
"message": "Only the creator of the group can remove users"
},
"createAccount": { "createAccount": {
"message": "Create Account" "message": "Create Account"
}, },

@ -17,8 +17,13 @@
d => !!d && !d.isBlocked() && d.isPrivate() && !d.isMe() && !!d.get('active_at') d => !!d && !d.isBlocked() && d.isPrivate() && !d.isMe() && !!d.get('active_at')
); );
if (!convo.isPublic()) { if (!convo.isPublic()) {
// filter our zombies and current members from the list of contact we can add
const members = convo.get('members') || []; 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'); this.chatName = convo.get('name');

@ -87,20 +87,12 @@
this.theme = groupConvo.theme; this.theme = groupConvo.theme;
if (this.isPublic) { if (this.isPublic) {
this.titleText = i18n('updateGroupDialogTitle', this.groupName); throw new Error('UpdateGroupMembersDialog is only made for Closed/Medium groups');
// 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); this.titleText = i18n('updateGroupDialogTitle', this.groupName);
// anybody can edit a closed group name or members // anybody can edit a closed group name or members
const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache();
this.isAdmin = groupConvo.isMediumGroup() this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK);
? true
: groupConvo.get('groupAdmins').includes(ourPK);
this.admins = groupConvo.get('groupAdmins'); this.admins = groupConvo.get('groupAdmins');
const convos = window const convos = window
.getConversationController() .getConversationController()
@ -108,6 +100,7 @@
.filter(d => !!d); .filter(d => !!d);
this.existingMembers = groupConvo.get('members') || []; this.existingMembers = groupConvo.get('members') || [];
this.existingZombies = groupConvo.get('zombies') || [];
// Show a contact if they are our friend or if they are a member // Show a contact if they are our friend or if they are a member
this.contactsAndMembers = convos.filter( this.contactsAndMembers = convos.filter(
d => this.existingMembers.includes(d.id) && d.isPrivate() && !d.isMe() d => this.existingMembers.includes(d.id) && d.isPrivate() && !d.isMe()
@ -118,7 +111,6 @@
if (!Array.isArray(this.existingMembers)) { if (!Array.isArray(this.existingMembers)) {
this.existingMembers = []; this.existingMembers = [];
} }
}
this.$el.focus(); this.$el.focus();
this.render(); this.render();
@ -133,6 +125,7 @@
cancelText: i18n('cancel'), cancelText: i18n('cancel'),
isPublic: this.isPublic, isPublic: this.isPublic,
existingMembers: this.existingMembers, existingMembers: this.existingMembers,
existingZombies: this.existingZombies,
contactList: this.contactsAndMembers, contactList: this.contactsAndMembers,
isAdmin: this.isAdmin, isAdmin: this.isAdmin,
admins: this.admins, admins: this.admins,
@ -149,15 +142,32 @@
async onSubmit(newMembers) { async onSubmit(newMembers) {
const _ = window.Lodash; const _ = window.Lodash;
const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); 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. // 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 notPresentInOld = allMembersAfterUpdate.filter(
const filteredMemberes = allMembers.filter(member => !_.includes(membersToRemove, member)); m => !allExistingMembersWithZombies.includes(m)
);
// be sure to include zombies in here
const membersToRemove = allExistingMembersWithZombies.filter(
m => !allMembersAfterUpdate.includes(m)
);
const xor = _.xor(membersToRemove, notPresentInOld); const xor = _.xor(membersToRemove, notPresentInOld);
if (xor.length === 0) { if (xor.length === 0) {
@ -166,10 +176,16 @@
return; 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( window.libsession.ClosedGroup.initiateGroupUpdate(
this.groupId, this.groupId,
this.groupName, this.groupName,
filteredMemberes, filteredMembers,
this.avatarPath this.avatarPath
); );
}, },

@ -1300,6 +1300,10 @@ input {
} }
} }
&.zombie {
opacity: 0.5;
}
&__checkmark { &__checkmark {
opacity: 0; opacity: 0;
transition: $session-transition-duration; transition: $session-transition-duration;

@ -10,6 +10,7 @@ import { initiateGroupUpdate } from '../../session/group';
import { ConversationModel, ConversationTypeEnum } from '../../models/conversation'; import { ConversationModel, ConversationTypeEnum } from '../../models/conversation';
import { getCompleteUrlForV2ConvoId } from '../../interactions/conversation'; import { getCompleteUrlForV2ConvoId } from '../../interactions/conversation';
import _ from 'lodash'; import _ from 'lodash';
import autoBind from 'auto-bind';
interface Props { interface Props {
contactList: Array<any>; contactList: Array<any>;
chatName: string; chatName: string;
@ -26,12 +27,7 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.onMemberClicked = this.onMemberClicked.bind(this); autoBind(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);
let contacts = this.props.contactList; let contacts = this.props.contactList;
@ -137,11 +133,10 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
private async submitForClosedGroup(pubkeys: Array<string>) { private async submitForClosedGroup(pubkeys: Array<string>) {
const { convo } = this.props; 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(); 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') || []; let existingMembers = convo.get('members') || [];
// at least make sure it's an array // at least make sure it's an array
if (!Array.isArray(existingMembers)) { if (!Array.isArray(existingMembers)) {

@ -6,6 +6,11 @@ import { SessionButton, SessionButtonColor } from '../session/SessionButton';
import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem'; import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem';
import { DefaultTheme } from 'styled-components'; import { DefaultTheme } from 'styled-components';
import { ToastUtils } from '../../session/utils'; 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 { interface Props {
titleText: string; titleText: string;
@ -15,10 +20,11 @@ interface Props {
// contacts not in the group // contacts not in the group
contactList: Array<any>; contactList: Array<any>;
isAdmin: boolean; isAdmin: boolean;
existingMembers: Array<String>; existingMembers: Array<string>;
admins: Array<String>; // used for closed group existingZombies: Array<string>;
admins: Array<string>; // used for closed group
i18n: any; i18n: LocalizerType;
onSubmit: any; onSubmit: any;
onClose: any; onClose: any;
theme: DefaultTheme; theme: DefaultTheme;
@ -26,23 +32,21 @@ interface Props {
interface State { interface State {
contactList: Array<ContactType>; contactList: Array<ContactType>;
zombies: Array<ContactType>;
errorDisplayed: boolean; errorDisplayed: boolean;
errorMessage: string; errorMessage: string;
} }
export class UpdateGroupMembersDialog extends React.Component<Props, State> { export class UpdateGroupMembersDialog extends React.Component<Props, State> {
constructor(props: any) { constructor(props: Props) {
super(props); super(props);
this.onMemberClicked = this.onMemberClicked.bind(this); autoBind(this);
this.onClickOK = this.onClickOK.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.closeDialog = this.closeDialog.bind(this);
let contacts = this.props.contactList; let contacts = this.props.contactList;
contacts = contacts.map(d => { contacts = contacts.map(d => {
const lokiProfile = d.getLokiProfile(); 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); const existingMember = this.props.existingMembers.includes(d.id);
@ -58,8 +62,33 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
}; };
}); });
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 = { this.state = {
contactList: contacts, contactList: contacts,
zombies,
errorDisplayed: false, errorDisplayed: false,
errorMessage: '', errorMessage: '',
}; };
@ -70,13 +99,14 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
public onClickOK() { public onClickOK() {
const members = this.getWouldBeMembers(this.state.contactList).map(d => d.id); 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.props.onSubmit(members);
this.closeDialog(); this.closeDialog();
} }
public render() { public render() {
const { okText, cancelText, contactList, titleText } = this.props; const { okText, cancelText, isAdmin, contactList, titleText } = this.props;
const showNoMembersMessage = contactList.length === 0; const showNoMembersMessage = contactList.length === 0;
@ -99,17 +129,20 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
<div className="spacer-md" /> <div className="spacer-md" />
<div className="group-member-list__selection">{this.renderMemberList()}</div> <div className="group-member-list__selection">{this.renderMemberList()}</div>
{this.renderZombiesList()}
{showNoMembersMessage && <p>{window.i18n('noMembersInThisGroup')}</p>} {showNoMembersMessage && <p>{window.i18n('noMembersInThisGroup')}</p>}
<div className="spacer-lg" /> <div className="spacer-lg" />
<div className="session-modal__button-group"> <div className="session-modal__button-group">
<SessionButton text={cancelText} onClick={this.closeDialog} /> <SessionButton text={cancelText} onClick={this.closeDialog} />
{isAdmin && (
<SessionButton <SessionButton
text={okText} text={okText}
onClick={this.onClickOK} onClick={this.onClickOK}
buttonColor={SessionButtonColor.Green} buttonColor={SessionButtonColor.Green}
/> />
)}
</div> </div>
</SessionModal> </SessionModal>
); );
@ -131,6 +164,21 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
)); ));
} }
private renderZombiesList() {
return this.state.zombies.map((member: ContactType, index: number) => (
<SessionMemberListItem
member={member}
index={index}
isSelected={!member.checkmarked}
onSelect={this.onZombieClicked}
onUnselect={this.onZombieClicked}
isZombie={true}
key={member.id}
theme={this.props.theme}
/>
));
}
private onKeyUp(event: any) { private onKeyUp(event: any) {
switch (event.key) { switch (event.key) {
case 'Enter': case 'Enter':
@ -158,10 +206,15 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
this.props.onClose(); this.props.onClose();
} }
private onMemberClicked(selected: any) { private onMemberClicked(selected: ContactType) {
const { isAdmin, admins } = this.props; const { isAdmin, admins } = this.props;
const { contactList } = this.state; const { contactList } = this.state;
if (!isAdmin) {
ToastUtils.pushOnlyAdminCanRemove();
return;
}
if (selected.existingMember && !isAdmin) { if (selected.existingMember && !isAdmin) {
window.log.warn('Only group admin can remove members!'); window.log.warn('Only group admin can remove members!');
return; return;
@ -190,4 +243,41 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
}; };
}); });
} }
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,
};
});
}
} }

@ -6,6 +6,7 @@ import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
import { Constants } from '../../session'; import { Constants } from '../../session';
import { DefaultTheme } from 'styled-components'; import { DefaultTheme } from 'styled-components';
import { PubKey } from '../../session/types'; import { PubKey } from '../../session/types';
import autoBind from 'auto-bind';
export interface ContactType { export interface ContactType {
id: string; id: string;
@ -22,6 +23,8 @@ interface Props {
member: ContactType; member: ContactType;
index: number; // index in the list index: number; // index in the list
isSelected: boolean; isSelected: boolean;
// this bool is used to make a zombie appear with less opacity than a normal member
isZombie?: boolean;
onSelect?: any; onSelect?: any;
onUnselect?: any; onUnselect?: any;
theme: DefaultTheme; theme: DefaultTheme;
@ -35,14 +38,11 @@ class SessionMemberListItemInner extends React.Component<Props> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.handleSelectionAction = this.handleSelectionAction.bind(this); autoBind(this);
this.selectMember = this.selectMember.bind(this);
this.unselectMember = this.unselectMember.bind(this);
this.renderAvatar = this.renderAvatar.bind(this);
} }
public render() { public render() {
const { isSelected, member } = this.props; const { isSelected, member, isZombie } = this.props;
const name = member.authorProfileName || PubKey.shorten(member.authorPhoneNumber); const name = member.authorProfileName || PubKey.shorten(member.authorPhoneNumber);
@ -51,7 +51,8 @@ class SessionMemberListItemInner extends React.Component<Props> {
className={classNames( className={classNames(
`session-member-item-${this.props.index}`, `session-member-item-${this.props.index}`,
'session-member-item', 'session-member-item',
isSelected && 'selected' isSelected && 'selected',
isZombie && 'zombie'
)} )}
onClick={this.handleSelectionAction} onClick={this.handleSelectionAction}
role="button" role="button"

@ -252,7 +252,7 @@ class SessionRightPanel extends React.Component<Props, State> {
const showUpdateGroupNameButton = isAdmin && !commonNoShow; const showUpdateGroupNameButton = isAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic; const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
const showUpdateGroupMembersButton = !isPublic && !commonNoShow && isAdmin; const showUpdateGroupMembersButton = !isPublic && !commonNoShow;
return ( return (
<div className="group-settings"> <div className="group-settings">

@ -52,7 +52,9 @@ export interface ConversationAttributes {
profileName?: string; profileName?: string;
id: string; id: string;
name?: string; name?: string;
// members are all members for this group. zombies excluded
members: Array<string>; members: Array<string>;
zombies: Array<string>; // only used for closed groups. Zombies are users which left but not yet removed by the admin
left: boolean; left: boolean;
expireTimer: number; expireTimer: number;
mentionedUs: boolean; mentionedUs: boolean;
@ -90,6 +92,7 @@ export interface ConversationAttributesOptionals {
id: string; id: string;
name?: string; name?: string;
members?: Array<string>; members?: Array<string>;
zombies?: Array<string>;
left?: boolean; left?: boolean;
expireTimer?: number; expireTimer?: number;
mentionedUs?: boolean; mentionedUs?: boolean;
@ -130,6 +133,7 @@ export const fillConvoAttributesWithDefaults = (
): ConversationAttributes => { ): ConversationAttributes => {
return _.defaults(optAttributes, { return _.defaults(optAttributes, {
members: [], members: [],
zombies: [],
left: false, left: false,
unreadCount: 0, unreadCount: 0,
lastMessageStatus: null, lastMessageStatus: null,

@ -451,6 +451,10 @@ export const downloadFileOpenGroupV2 = async (
fileId: number, fileId: number,
roomInfos: OpenGroupRequestCommonType roomInfos: OpenGroupRequestCommonType
): Promise<Uint8Array | null> => { ): Promise<Uint8Array | null> => {
if (!fileId) {
window.log.warn('downloadFileOpenGroupV2: FileId cannot be unset. returning null');
return null;
}
const request: OpenGroupV2Request = { const request: OpenGroupV2Request = {
method: 'GET', method: 'GET',
room: roomInfos.roomId, room: roomInfos.roomId,

@ -584,6 +584,9 @@ async function handleClosedGroupMembersAdded(
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp));
convo.set({ members }); convo.set({ members });
// make sure those members are not on our zombie list
addedMembers.forEach(added => removeMemberFromZombies(envelope, PubKey.cast(added), convo));
convo.updateLastMessage(); convo.updateLastMessage();
await convo.commit(); await convo.commit();
await removeFromCache(envelope); await removeFromCache(envelope);
@ -633,16 +636,9 @@ async function handleClosedGroupMembersRemoved(
const ourPubKey = UserUtils.getOurPubKeyFromCache(); const ourPubKey = UserUtils.getOurPubKeyFromCache();
const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key);
if (wasCurrentUserRemoved) { if (wasCurrentUserRemoved) {
await markGroupAsLeftOrKicked(groupPublicKey, convo, true); await markGroupAsLeftOrKicked(groupPubKey, 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.');
}
} }
// 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 // Only add update message if we have something to show
if (membersAfterUpdate.length !== currentMembers.length) { if (membersAfterUpdate.length !== currentMembers.length) {
@ -660,6 +656,8 @@ async function handleClosedGroupMembersRemoved(
// Update the group // Update the group
convo.set({ members: membersAfterUpdate }); convo.set({ members: membersAfterUpdate });
const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z));
convo.set({ zombies });
await convo.commit(); await convo.commit();
await removeFromCache(envelope); 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 // otherwise, we remove the sender from the list of current members in this group
const oldMembers = convo.get('members') || []; const oldMembers = convo.get('members') || [];
const newMembers = oldMembers.filter(s => s !== sender); 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) // Show log if we sent this message ourself (from another device or not)
if (UserUtils.isUsFromCache(sender)) { if (UserUtils.isUsFromCache(sender)) {
@ -784,12 +783,6 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver
return; 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. // 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) // But this member was in the list of members (as performIfValid checks for that)
const groupDiff: ClosedGroup.GroupDiff = { const groupDiff: ClosedGroup.GroupDiff = {
@ -797,8 +790,19 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver
}; };
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp));
convo.updateLastMessage(); 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); 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 convo.commit();
await removeFromCache(envelope); await removeFromCache(envelope);

@ -40,7 +40,8 @@ import { updateOpenGroupV2 } from '../../opengroup/opengroupV2/OpenGroupUpdate';
export interface GroupInfo { export interface GroupInfo {
id: string; id: string;
name: string; name: string;
members: Array<string>; // Primary keys members: Array<string>;
zombies?: Array<string>;
active?: boolean; active?: boolean;
expireTimer?: number | null; expireTimer?: number | null;
avatar?: any; avatar?: any;
@ -107,6 +108,8 @@ export async function initiateGroupUpdate(
if (!isMediumGroup) { if (!isMediumGroup) {
throw new Error('Legacy group are not supported anymore.'); 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 // 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. // updateOrCreateClosedGroup() will update them if given the choice.
@ -114,10 +117,13 @@ export async function initiateGroupUpdate(
id: groupId, id: groupId,
name: groupName, name: groupName,
members, 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, active: true,
expireTimer: convo.get('expireTimer'), expireTimer: convo.get('expireTimer'),
avatar, avatar,
}; };
console.warn('initiategroupUpdate new zombies:', groupDetails.zombies);
const diff = buildGroupDiff(convo, groupDetails); const diff = buildGroupDiff(convo, groupDetails);
@ -150,8 +156,9 @@ export async function initiateGroupUpdate(
const dbMessageLeaving = await addUpdateMessage(convo, leavingOnlyDiff, 'outgoing', Date.now()); const dbMessageLeaving = await addUpdateMessage(convo, leavingOnlyDiff, 'outgoing', Date.now());
MessageController.getInstance().register(dbMessageLeaving.id, dbMessageLeaving); MessageController.getInstance().register(dbMessageLeaving.id, dbMessageLeaving);
const stillMembers = members; 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( export async function addUpdateMessage(
@ -253,6 +260,10 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
updates.left = true; updates.left = true;
} }
if (details.zombies) {
updates.zombies = details.zombies;
}
conversation.set(updates); conversation.set(updates);
// Update the conversation avatar only if new avatar exists and hash differs // 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') { if (expireTimer === undefined || typeof expireTimer !== 'number') {
return; return;
} }
const source = UserUtils.getOurPubKeyStrFromCache(); await conversation.updateExpirationTimer(
await conversation.updateExpirationTimer(expireTimer, source, Date.now(), { expireTimer,
UserUtils.getOurPubKeyStrFromCache(),
Date.now(),
{
fromSync: true, fromSync: true,
}); }
);
} }
export async function leaveClosedGroup(groupId: string) { export async function leaveClosedGroup(groupId: string) {
@ -420,11 +435,11 @@ async function sendAddedMembers(
await Promise.all(promises); await Promise.all(promises);
} }
async function sendRemovedMembers( export async function sendRemovedMembers(
convo: ConversationModel, convo: ConversationModel,
removedMembers: Array<string>, removedMembers: Array<string>,
messageId: string, stillMembers: Array<string>,
stillMembers: Array<string> messageId?: string
) { ) {
if (!removedMembers?.length) { if (!removedMembers?.length) {
window.log.warn('No removedMembers given for group update. Skipping'); window.log.warn('No removedMembers given for group update. Skipping');

@ -183,6 +183,14 @@ export function pushCannotRemoveCreatorFromGroup() {
); );
} }
export function pushOnlyAdminCanRemove() {
pushToastInfo(
'onlyAdminCanRemoveMembers',
window.i18n('onlyAdminCanRemoveMembers'),
window.i18n('onlyAdminCanRemoveMembersDesc')
);
}
export function pushUserNeedsToHaveJoined() { export function pushUserNeedsToHaveJoined() {
pushToastWarning( pushToastWarning(
'userNeedsToHaveJoined', 'userNeedsToHaveJoined',

@ -81,6 +81,7 @@ export class MockConversation {
lastJoinedTimestamp: Date.now(), lastJoinedTimestamp: Date.now(),
lastMessageStatus: null, lastMessageStatus: null,
lastMessage: null, lastMessage: null,
zombies: [],
}; };
} }

Loading…
Cancel
Save