You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/components/dialog/UpdateGroupMembersDialog.tsx

217 lines
6.9 KiB
TypeScript

import _ from 'lodash';
import { useDispatch } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { ToastUtils, UserUtils } from '../../session/utils';
import { updateGroupMembersModal } from '../../state/ducks/modalDialog';
import { MemberListItem } from '../MemberListItem';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SpacerLG } from '../basic/Text';
import { useConversationPropsById, useWeAreAdmin } from '../../hooks/useParamSelector';
import { useSet } from '../../hooks/useSet';
import { getConversationController } from '../../session/conversations';
import { initiateClosedGroupUpdate } from '../../session/group/closed-group';
type Props = {
conversationId: string;
};
const StyledClassicMemberList = styled.div`
max-height: 240px;
`;
/**
* Admins are always put first in the list of group members.
* Also, admins have a little crown on their avatar.
*/
const ClassicMemberList = (props: {
convoId: string;
selectedMembers: Array<string>;
onSelect: (m: string) => void;
onUnselect: (m: string) => void;
}) => {
const { onSelect, convoId, onUnselect, selectedMembers } = props;
const weAreAdmin = useWeAreAdmin(convoId);
const convoProps = useConversationPropsById(convoId);
if (!convoProps) {
throw new Error('MemberList needs convoProps');
}
let currentMembers = convoProps.members || [];
const { groupAdmins } = convoProps;
currentMembers = [...currentMembers].sort(m => (groupAdmins?.includes(m) ? -1 : 0));
return (
<>
{currentMembers.map(member => {
const isSelected = (weAreAdmin && selectedMembers.includes(member)) || false;
const isAdmin = groupAdmins?.includes(member);
return (
<MemberListItem
key={`classic-member-list-${member}`}
pubkey={member}
isSelected={isSelected}
onSelect={onSelect}
onUnselect={onUnselect}
isAdmin={isAdmin}
disableBg={true}
/>
);
})}
</>
);
};
async function onSubmit(convoId: string, membersAfterUpdate: Array<string>) {
const convoFound = getConversationController().get(convoId);
if (!convoFound || !convoFound.isGroup()) {
throw new Error('Invalid convo for updateGroupMembersDialog');
}
if (!convoFound.isAdmin(UserUtils.getOurPubKeyStrFromCache())) {
window.log.warn('Skipping update of members, we are not the admin');
return;
}
const ourPK = UserUtils.getOurPubKeyStrFromCache();
const allMembersAfterUpdate = _.uniq(_.concat(membersAfterUpdate, [ourPK]));
// membersAfterUpdate 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 merge all members, including zombies for this call.
// 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 existingMembers = convoFound.get('members') || [];
const existingZombies = convoFound.get('zombies') || [];
const allExistingMembersWithZombies = _.uniq(existingMembers.concat(existingZombies));
const notPresentInOld = allMembersAfterUpdate.filter(
m => !allExistingMembersWithZombies.includes(m)
);
// be sure to include zombies in here
const membersToRemove = allExistingMembersWithZombies.filter(
m => !allMembersAfterUpdate.includes(m)
);
// do the xor between the two. if the length is 0, it means the before and the after is the same.
const xor = _.xor(membersToRemove, notPresentInOld);
if (xor.length === 0) {
window.log.info('skipping group update: no detected changes in group member list');
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(
memberAfterUpdate => !_.includes(membersToRemove, memberAfterUpdate)
);
void initiateClosedGroupUpdate(
convoId,
convoFound.get('displayNameInProfile') || 'Unknown',
filteredMembers
);
}
export const UpdateGroupMembersDialog = (props: Props) => {
const { conversationId } = props;
const convoProps = useConversationPropsById(conversationId);
const existingMembers = convoProps?.members || [];
const {
addTo,
removeFrom,
uniqueValues: membersToKeepWithUpdate,
} = useSet<string>(existingMembers);
const dispatch = useDispatch();
if (!convoProps || convoProps.isPrivate || convoProps.isPublic) {
throw new Error('UpdateGroupMembersDialog invalid convoProps');
}
const weAreAdmin = convoProps.weAreAdmin || false;
const closeDialog = () => {
dispatch(updateGroupMembersModal(null));
};
const onClickOK = async () => {
// const members = getWouldBeMembers(this.state.contactList).map(d => d.id);
// do not include zombies here, they are removed by force
await onSubmit(conversationId, membersToKeepWithUpdate);
closeDialog();
};
useKey((event: KeyboardEvent) => {
return event.key === 'Esc' || event.key === 'Escape';
}, closeDialog);
const onAdd = (member: string) => {
if (!weAreAdmin) {
window?.log?.warn('Only group admin can add members!');
return;
}
addTo(member);
};
const onRemove = (member: string) => {
if (!weAreAdmin) {
window?.log?.warn('Only group admin can remove members!');
return;
}
if (convoProps.groupAdmins?.includes(member)) {
ToastUtils.pushCannotRemoveCreatorFromGroup();
window?.log?.warn(
`User ${member} cannot be removed as they are the creator of the closed group.`
);
return;
}
removeFrom(member);
};
const showNoMembersMessage = existingMembers.length === 0;
const okText = window.i18n('okay');
const cancelText = window.i18n('cancel');
const titleText = window.i18n('groupMembers');
return (
<SessionWrapperModal title={titleText} onClose={closeDialog}>
<StyledClassicMemberList className="contact-selection-list">
<ClassicMemberList
convoId={conversationId}
onSelect={onAdd}
onUnselect={onRemove}
selectedMembers={membersToKeepWithUpdate}
/>
</StyledClassicMemberList>
{showNoMembersMessage && <p>{window.i18n('groupMembersNone')}</p>}
<SpacerLG />
<div className="session-modal__button-group">
{weAreAdmin && (
<SessionButton text={okText} onClick={onClickOK} buttonType={SessionButtonType.Simple} />
)}
<SessionButton
text={cancelText}
buttonColor={weAreAdmin ? SessionButtonColor.Danger : undefined}
buttonType={SessionButtonType.Simple}
onClick={closeDialog}
/>
</div>
</SessionWrapperModal>
);
};