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.
246 lines
8.5 KiB
TypeScript
246 lines
8.5 KiB
TypeScript
import { useState } from 'react';
|
|
import useKey from 'react-use/lib/useKey';
|
|
|
|
import { PubkeyType } from 'libsession_util_nodejs';
|
|
import _, { difference, uniq } from 'lodash';
|
|
import { useDispatch } from 'react-redux';
|
|
import { VALIDATION } from '../../session/constants';
|
|
import { ConvoHub } from '../../session/conversations';
|
|
import { ToastUtils, UserUtils } from '../../session/utils';
|
|
import { updateGroupMembersModal, updateInviteContactModal } from '../../state/ducks/modalDialog';
|
|
import { SpacerLG } from '../basic/Text';
|
|
|
|
import {
|
|
useIsPrivate,
|
|
useIsPublic,
|
|
useSortedGroupMembers,
|
|
useZombies,
|
|
} from '../../hooks/useParamSelector';
|
|
import { useSet } from '../../hooks/useSet';
|
|
import { ClosedGroup } from '../../session/group/closed-group';
|
|
import { PubKey } from '../../session/types';
|
|
import { SessionUtilUserGroups } from '../../session/utils/libsession/libsession_utils_user_groups';
|
|
import { groupInfoActions } from '../../state/ducks/metaGroups';
|
|
import { useContactsToInviteToGroup } from '../../state/selectors/conversations';
|
|
import { useSelectedIsGroupV2 } from '../../state/selectors/selectedConversation';
|
|
import { MemberListItem } from '../MemberListItem';
|
|
import { SessionWrapperModal } from '../SessionWrapperModal';
|
|
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
|
|
import { SessionToggle } from '../basic/SessionToggle';
|
|
import { GroupInviteRequiredVersionBanner } from '../NoticeBanner';
|
|
import { hasClosedGroupV2QAButtons } from '../../shared/env_vars';
|
|
import { ConversationTypeEnum } from '../../models/types';
|
|
import { Localizer } from '../basic/Localizer';
|
|
|
|
type Props = {
|
|
conversationId: string;
|
|
};
|
|
|
|
async function submitForOpenGroup(convoId: string, pubkeys: Array<string>) {
|
|
const convo = ConvoHub.use().get(convoId);
|
|
if (!convo || !convo.isPublic()) {
|
|
throw new Error('submitForOpenGroup group not found');
|
|
}
|
|
try {
|
|
const roomDetails = await SessionUtilUserGroups.getCommunityByConvoIdNotCached(convo.id);
|
|
if (!roomDetails) {
|
|
throw new Error(`getCommunityByFullUrl returned no result for ${convo.id}`);
|
|
}
|
|
const groupInvitation = {
|
|
url: roomDetails?.fullUrlWithPubkey,
|
|
name: convo.getNicknameOrRealUsernameOrPlaceholder(),
|
|
};
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
pubkeys.forEach(async pubkeyStr => {
|
|
const privateConvo = await ConvoHub.use().getOrCreateAndWait(
|
|
pubkeyStr,
|
|
ConversationTypeEnum.PRIVATE
|
|
);
|
|
|
|
if (privateConvo) {
|
|
void privateConvo.sendMessage({
|
|
body: '',
|
|
attachments: undefined,
|
|
groupInvitation,
|
|
preview: undefined,
|
|
quote: undefined,
|
|
});
|
|
}
|
|
});
|
|
} catch (e) {
|
|
window.log.warn('submitForOpenGroup failed with:', e.message);
|
|
}
|
|
}
|
|
|
|
const submitForClosedGroup = async (convoId: string, pubkeys: Array<string>) => {
|
|
const convo = ConvoHub.use().get(convoId);
|
|
if (!convo || !convo.isGroup()) {
|
|
throw new Error('submitForClosedGroup group not found');
|
|
}
|
|
// 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.getGroupMembers() || [];
|
|
// at least make sure it's an array
|
|
if (!Array.isArray(existingMembers)) {
|
|
existingMembers = [];
|
|
}
|
|
existingMembers = _.compact(existingMembers);
|
|
const existingZombies = convo.getGroupZombies() || [];
|
|
const newMembers = pubkeys.filter(d => !existingMembers.includes(d));
|
|
|
|
if (newMembers.length > 0) {
|
|
// Do not trigger an update if there is too many members
|
|
// be sure to include current zombies in this count
|
|
if (
|
|
newMembers.length + existingMembers.length + existingZombies.length >
|
|
VALIDATION.CLOSED_GROUP_SIZE_LIMIT
|
|
) {
|
|
ToastUtils.pushTooManyMembers();
|
|
return;
|
|
}
|
|
|
|
const allMembers = _.concat(existingMembers, newMembers, [ourPK]);
|
|
const uniqMembers = _.uniq(allMembers);
|
|
|
|
const groupId = convo.get('id');
|
|
const groupName = convo.getNicknameOrRealUsernameOrPlaceholder();
|
|
|
|
await ClosedGroup.initiateClosedGroupUpdate(groupId, groupName, uniqMembers);
|
|
}
|
|
};
|
|
|
|
const InviteContactsDialogInner = (props: Props) => {
|
|
const { conversationId } = props;
|
|
const dispatch = useDispatch();
|
|
|
|
const privateContactPubkeys = useContactsToInviteToGroup() as Array<PubkeyType>;
|
|
const isPrivate = useIsPrivate(conversationId);
|
|
const isPublic = useIsPublic(conversationId);
|
|
const membersFromRedux = useSortedGroupMembers(conversationId) || [];
|
|
const zombiesFromRedux = useZombies(conversationId) || [];
|
|
const isGroupV2 = useSelectedIsGroupV2();
|
|
const [shareHistory, setShareHistory] = useState(false);
|
|
|
|
const { uniqueValues: selectedContacts, addTo, removeFrom, empty } = useSet<string>();
|
|
|
|
if (isPrivate) {
|
|
throw new Error('InviteContactsDialogInner must be a group');
|
|
}
|
|
const zombiesAndMembers = uniq([...membersFromRedux, ...zombiesFromRedux]);
|
|
// filter our zombies and current members from the list of contact we can add
|
|
|
|
const validContactsForInvite = isPublic
|
|
? privateContactPubkeys
|
|
: difference(privateContactPubkeys, zombiesAndMembers);
|
|
|
|
const closeDialog = () => {
|
|
dispatch(updateInviteContactModal(null));
|
|
};
|
|
|
|
const onClickOK = () => {
|
|
if (selectedContacts.length <= 0) {
|
|
closeDialog();
|
|
return;
|
|
}
|
|
if (isPublic) {
|
|
void submitForOpenGroup(conversationId, selectedContacts);
|
|
return;
|
|
}
|
|
if (PubKey.is03Pubkey(conversationId)) {
|
|
const forcedAsPubkeys = selectedContacts as Array<PubkeyType>;
|
|
const action = groupInfoActions.currentDeviceGroupMembersChange({
|
|
addMembersWithoutHistory: shareHistory ? [] : forcedAsPubkeys,
|
|
addMembersWithHistory: shareHistory ? forcedAsPubkeys : [],
|
|
removeMembers: [],
|
|
groupPk: conversationId,
|
|
alsoRemoveMessages: false,
|
|
});
|
|
dispatch(action as any);
|
|
empty();
|
|
// We want to show the dialog where "invite sending" is visible (i.e. the current group members) instead of this one
|
|
// once we hit "invite"
|
|
closeDialog();
|
|
dispatch(updateGroupMembersModal({ conversationId }));
|
|
|
|
return;
|
|
}
|
|
void submitForClosedGroup(conversationId, selectedContacts);
|
|
};
|
|
|
|
useKey((event: KeyboardEvent) => {
|
|
return event.key === 'Enter';
|
|
}, onClickOK);
|
|
|
|
useKey((event: KeyboardEvent) => {
|
|
return event.key === 'Esc' || event.key === 'Escape';
|
|
}, closeDialog);
|
|
|
|
const titleText = window.i18n('membersInvite');
|
|
const cancelText = window.i18n('cancel');
|
|
const okText = window.i18n('okay');
|
|
|
|
const hasContacts = validContactsForInvite.length > 0;
|
|
|
|
return (
|
|
<SessionWrapperModal title={titleText} onClose={closeDialog} showExitIcon={true}>
|
|
{hasContacts && isGroupV2 && <GroupInviteRequiredVersionBanner />}
|
|
|
|
<SpacerLG />
|
|
|
|
{/* TODO: localize those strings once out releasing those buttons for real Remove after QA */}
|
|
{isGroupV2 && hasClosedGroupV2QAButtons() && (
|
|
<>
|
|
<span style={{ display: 'flex', alignItems: 'center' }}>
|
|
Share History?{' '}
|
|
<SessionToggle active={shareHistory} onClick={() => setShareHistory(!shareHistory)} />
|
|
</span>
|
|
</>
|
|
)}
|
|
<div className="contact-selection-list">
|
|
{hasContacts ? (
|
|
validContactsForInvite.map((member: string) => (
|
|
<MemberListItem
|
|
key={`contacts-list-${member}`}
|
|
pubkey={member}
|
|
isSelected={selectedContacts.includes(member)}
|
|
onSelect={addTo}
|
|
onUnselect={removeFrom}
|
|
disableBg={true}
|
|
maxNameWidth="100%"
|
|
/>
|
|
))
|
|
) : (
|
|
<>
|
|
<SpacerLG />
|
|
<p className="no-contacts">
|
|
<Localizer token="contactNone" />
|
|
</p>
|
|
<SpacerLG />
|
|
</>
|
|
)}
|
|
</div>
|
|
<SpacerLG />
|
|
<SpacerLG />
|
|
<div className="session-modal__button-group">
|
|
<SessionButton
|
|
text={okText}
|
|
buttonType={SessionButtonType.Simple}
|
|
disabled={!hasContacts}
|
|
onClick={onClickOK}
|
|
dataTestId="session-confirm-ok-button"
|
|
/>
|
|
<SessionButton
|
|
text={cancelText}
|
|
buttonColor={SessionButtonColor.Danger}
|
|
buttonType={SessionButtonType.Simple}
|
|
onClick={closeDialog}
|
|
dataTestId="session-confirm-cancel-button"
|
|
/>
|
|
</div>
|
|
</SessionWrapperModal>
|
|
);
|
|
};
|
|
|
|
export const InviteContactsDialog = InviteContactsDialogInner;
|