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; 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 ( ); })} ); }; async function onSubmit(convoId: string, membersAfterUpdate: Array) { 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(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 ( {showNoMembersMessage &&

{window.i18n('groupMembersNone')}

}
{weAreAdmin && ( )}
); };