New Group administration: Add users

This commit is contained in:
Evan Hahn 2021-03-11 15:29:31 -06:00 committed by Josh Perez
parent e81c18e84c
commit b81a52bbdd
43 changed files with 1789 additions and 277 deletions

View file

@ -35,6 +35,7 @@ import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelectedContactForGroupAddition';
// State
@ -2273,50 +2274,23 @@ export function reducer(
return state;
}
const { selectedConversationIds: oldSelectedConversationIds } = composer;
let {
maximumGroupSizeModalState,
recommendedGroupSizeModalState,
} = composer;
const {
conversationId,
maxGroupSize,
maxRecommendedGroupSize,
} = action.payload;
const selectedConversationIds = without(
oldSelectedConversationIds,
conversationId
);
const shouldAdd =
selectedConversationIds.length === oldSelectedConversationIds.length;
if (shouldAdd) {
// 1 for you, 1 for the new contact.
const newExpectedMemberCount = selectedConversationIds.length + 2;
if (newExpectedMemberCount > maxGroupSize) {
return state;
}
if (
newExpectedMemberCount === maxGroupSize &&
maximumGroupSizeModalState === OneTimeModalState.NeverShown
) {
maximumGroupSizeModalState = OneTimeModalState.Showing;
} else if (
newExpectedMemberCount >= maxRecommendedGroupSize &&
recommendedGroupSizeModalState === OneTimeModalState.NeverShown
) {
recommendedGroupSizeModalState = OneTimeModalState.Showing;
}
selectedConversationIds.push(conversationId);
}
return {
...state,
composer: {
...composer,
maximumGroupSizeModalState,
recommendedGroupSizeModalState,
selectedConversationIds,
...toggleSelectedContactForGroupAddition(
action.payload.conversationId,
{
maxGroupSize: action.payload.maxGroupSize,
maxRecommendedGroupSize: action.payload.maxRecommendedGroupSize,
maximumGroupSizeModalState: composer.maximumGroupSizeModalState,
// We say you're already in the group, even though it hasn't been created yet.
numberOfContactsAlreadyInGroup: 1,
recommendedGroupSizeModalState:
composer.recommendedGroupSizeModalState,
selectedConversationIds: composer.selectedConversationIds,
}
),
},
};
}

View file

@ -4,7 +4,6 @@
import memoizee from 'memoizee';
import { fromPairs, isNumber, isString } from 'lodash';
import { createSelector } from 'reselect';
import Fuse, { FuseOptions } from 'fuse.js';
import { StateType } from '../reducer';
import {
@ -29,6 +28,7 @@ import { PropsDataType as TimelinePropsType } from '../../components/conversatio
import { TimelineItemType } from '../../components/conversation/TimelineItem';
import { assert } from '../../util/assert';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { filterAndSortContacts } from '../../util/filterAndSortContacts';
import {
getInteractionMode,
@ -342,14 +342,14 @@ export const getComposerContactSearchTerm = createSelector(
);
/**
* This returns contacts for the composer, which isn't just your primary's system
* contacts. It may include false positives, which is better than missing contacts.
* This returns contacts for the composer and group members, which isn't just your primary
* system contacts. It may include false positives, which is better than missing contacts.
*
* Because it filters unregistered contacts and that's (partially) determined by the
* current time, it's possible for this to return stale contacts that have unregistered
* if no other conversations change. This should be a rare false positive.
*/
const getContacts = createSelector(
export const getContacts = createSelector(
getConversationLookup,
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
Object.values(conversationLookup).filter(
@ -371,13 +371,6 @@ const getNoteToSelfTitle = createSelector(getIntl, (i18n: LocalizerType) =>
i18n('noteToSelf').toLowerCase()
);
const COMPOSE_CONTACTS_FUSE_OPTIONS: FuseOptions<ConversationType> = {
// A small-but-nonzero threshold lets us match parts of E164s better, and makes the
// search a little more forgiving.
threshold: 0.05,
keys: ['title', 'name', 'e164'],
};
export const getComposeContacts = createSelector(
getNormalizedComposerContactSearchTerm,
getContacts,
@ -389,55 +382,21 @@ export const getComposeContacts = createSelector(
noteToSelf: ConversationType,
noteToSelfTitle: string
): Array<ConversationType> => {
let result: Array<ConversationType>;
if (searchTerm.length) {
const fuse = new Fuse<ConversationType>(
contacts,
COMPOSE_CONTACTS_FUSE_OPTIONS
);
result = fuse.search(searchTerm);
if (noteToSelfTitle.includes(searchTerm)) {
result.push(noteToSelf);
}
} else {
result = contacts.concat();
result.sort((a, b) => collator.compare(a.title, b.title));
const result: Array<ConversationType> = filterAndSortContacts(
contacts,
searchTerm
);
if (!searchTerm || noteToSelfTitle.includes(searchTerm)) {
result.push(noteToSelf);
}
return result;
}
);
/*
* This returns contacts for the composer when you're picking new group members. It casts
* a wider net than `getContacts`.
*/
const getGroupContacts = createSelector(
getConversationLookup,
(conversationLookup): Array<ConversationType> =>
Object.values(conversationLookup).filter(
contact =>
contact.type === 'direct' &&
!contact.isMe &&
!contact.isBlocked &&
!isConversationUnregistered(contact)
)
);
export const getCandidateGroupContacts = createSelector(
export const getCandidateContactsForNewGroup = createSelector(
getContacts,
getNormalizedComposerContactSearchTerm,
getGroupContacts,
(searchTerm, contacts): Array<ConversationType> => {
if (searchTerm.length) {
return new Fuse<ConversationType>(
contacts,
COMPOSE_CONTACTS_FUSE_OPTIONS
).search(searchTerm);
}
return contacts.concat().sort((a, b) => collator.compare(a.title, b.title));
}
filterAndSortContacts
);
export const getCantAddContactForModal = createSelector(

View file

@ -8,11 +8,15 @@ import {
ConversationDetails,
StateProps,
} from '../../components/conversation/conversation-details/ConversationDetails';
import { getConversationSelector } from '../selectors/conversations';
import {
getContacts,
getConversationSelector,
} from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { MediaItemType } from '../../components/LightboxGallery';
export type SmartConversationDetailsProps = {
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
conversationId: string;
hasGroupLink: boolean;
loadRecentMediaItems: (limit: number) => void;
@ -46,10 +50,12 @@ const mapStateToProps = (
? conversation.canEditGroupInfo
: false;
const isAdmin = Boolean(conversation?.areWeAdmin);
const candidateContactsToAdd = getContacts(state);
return {
...props,
canEditGroupInfo,
candidateContactsToAdd,
conversation,
i18n: getIntl(state),
isAdmin,

View file

@ -16,7 +16,7 @@ import { ComposerStep, OneTimeModalState } from '../ducks/conversations';
import { getSearchResults, isSearching } from '../selectors/search';
import { getIntl, getRegionCode } from '../selectors/user';
import {
getCandidateGroupContacts,
getCandidateContactsForNewGroup,
getCantAddContactForModal,
getComposeContacts,
getComposeGroupAvatar,
@ -102,7 +102,7 @@ const getModeSpecificProps = (
case ComposerStep.ChooseGroupMembers:
return {
mode: LeftPaneMode.ChooseGroupMembers,
candidateContacts: getCandidateGroupContacts(state),
candidateContacts: getCandidateContactsForNewGroup(state),
cantAddContactForModal: getCantAddContactForModal(state),
isShowingRecommendedGroupSizeModal:
getRecommendedGroupSizeModalState(state) ===