Fix inaccurate numbers on group details screen

This commit is contained in:
Evan Hahn 2021-05-13 09:47:30 -05:00 committed by Scott Nonnenberg
parent 984b5e2b44
commit a8346c490e
8 changed files with 324 additions and 52 deletions

View file

@ -45,6 +45,13 @@ const createProps = (hasGroupLink = false): Props => ({
isMe: i === 2, isMe: i === 2,
}), }),
})), })),
pendingApprovalMemberships: times(8, () => ({
member: getDefaultConversation(),
})),
pendingMemberships: times(5, () => ({
metadata: {},
member: getDefaultConversation(),
})),
setDisappearingMessages: action('setDisappearingMessages'), setDisappearingMessages: action('setDisappearingMessages'),
showAllMedia: action('showAllMedia'), showAllMedia: action('showAllMedia'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),

View file

@ -22,6 +22,10 @@ import {
ConversationDetailsMembershipList, ConversationDetailsMembershipList,
GroupV2Membership, GroupV2Membership,
} from './ConversationDetailsMembershipList'; } from './ConversationDetailsMembershipList';
import {
GroupV2PendingMembership,
GroupV2RequestingMembership,
} from './PendingInvites';
import { EditConversationAttributesModal } from './EditConversationAttributesModal'; import { EditConversationAttributesModal } from './EditConversationAttributesModal';
import { RequestState } from './util'; import { RequestState } from './util';
@ -41,6 +45,8 @@ export type StateProps = {
isAdmin: boolean; isAdmin: boolean;
loadRecentMediaItems: (limit: number) => void; loadRecentMediaItems: (limit: number) => void;
memberships: Array<GroupV2Membership>; memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
setDisappearingMessages: (seconds: number) => void; setDisappearingMessages: (seconds: number) => void;
showAllMedia: () => void; showAllMedia: () => void;
showContactModal: (conversationId: string) => void; showContactModal: (conversationId: string) => void;
@ -77,6 +83,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
isAdmin, isAdmin,
loadRecentMediaItems, loadRecentMediaItems,
memberships, memberships,
pendingApprovalMemberships,
pendingMemberships,
setDisappearingMessages, setDisappearingMessages,
showAllMedia, showAllMedia,
showContactModal, showContactModal,
@ -108,9 +116,6 @@ export const ConversationDetails: React.ComponentType<Props> = ({
throw new Error('ConversationDetails rendered without a conversation'); throw new Error('ConversationDetails rendered without a conversation');
} }
const pendingMemberships = conversation.pendingMemberships || [];
const pendingApprovalMemberships =
conversation.pendingApprovalMemberships || [];
const invitesCount = const invitesCount =
pendingMemberships.length + pendingApprovalMemberships.length; pendingMemberships.length + pendingApprovalMemberships.length;
@ -213,6 +218,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
canEdit={canEditGroupInfo} canEdit={canEditGroupInfo}
conversation={conversation} conversation={conversation}
i18n={i18n} i18n={i18n}
memberships={memberships}
startEditing={() => { startEditing={() => {
setModalState(ModalState.EditingGroupAttributes); setModalState(ModalState.EditingGroupAttributes);
}} }}

View file

@ -27,7 +27,6 @@ const createConversation = (): ConversationType =>
type: 'group', type: 'group',
lastUpdated: 0, lastUpdated: 0,
title: text('conversation title', 'Some Conversation'), title: text('conversation title', 'Some Conversation'),
memberships: new Array(number('conversation members length', 0)),
}); });
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
@ -35,6 +34,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n, i18n,
canEdit: false, canEdit: false,
startEditing: action('startEditing'), startEditing: action('startEditing'),
memberships: new Array(number('conversation members length', 0)),
...overrideProps, ...overrideProps,
}); });

View file

@ -7,12 +7,14 @@ import { Avatar } from '../../Avatar';
import { Emojify } from '../Emojify'; import { Emojify } from '../Emojify';
import { LocalizerType } from '../../../types/Util'; import { LocalizerType } from '../../../types/Util';
import { ConversationType } from '../../../state/ducks/conversations'; import { ConversationType } from '../../../state/ducks/conversations';
import { GroupV2Membership } from './ConversationDetailsMembershipList';
import { bemGenerator } from './util'; import { bemGenerator } from './util';
export type Props = { export type Props = {
canEdit: boolean; canEdit: boolean;
conversation: ConversationType; conversation: ConversationType;
i18n: LocalizerType; i18n: LocalizerType;
memberships: Array<GroupV2Membership>;
startEditing: () => void; startEditing: () => void;
}; };
@ -22,10 +24,9 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
canEdit, canEdit,
conversation, conversation,
i18n, i18n,
memberships,
startEditing, startEditing,
}) => { }) => {
const memberships = conversation.memberships || [];
const contents = ( const contents = (
<> <>
<Avatar <Avatar

View file

@ -12,10 +12,9 @@ import {
getCandidateContactsForNewGroup, getCandidateContactsForNewGroup,
getConversationByIdSelector, getConversationByIdSelector,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { GroupV2Membership } from '../../components/conversation/conversation-details/ConversationDetailsMembershipList'; import { getGroupMemberships } from '../../util/getGroupMemberships';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { MediaItemType } from '../../components/LightboxGallery'; import { MediaItemType } from '../../components/LightboxGallery';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { assert } from '../../util/assert'; import { assert } from '../../util/assert';
export type SmartConversationDetailsProps = { export type SmartConversationDetailsProps = {
@ -59,17 +58,6 @@ const mapStateToProps = (
? conversation.canEditGroupInfo ? conversation.canEditGroupInfo
: false; : false;
const memberships = (conversation.memberships || []).reduce(
(result: Array<GroupV2Membership>, membership) => {
const member = conversationSelector(membership.conversationId);
if (!member || isConversationUnregistered(member)) {
return result;
}
return [...result, { isAdmin: membership.isAdmin, member }];
},
[]
);
const isAdmin = Boolean(conversation?.areWeAdmin); const isAdmin = Boolean(conversation?.areWeAdmin);
const candidateContactsToAdd = getCandidateContactsForNewGroup(state); const candidateContactsToAdd = getCandidateContactsForNewGroup(state);
@ -80,7 +68,7 @@ const mapStateToProps = (
conversation, conversation,
i18n: getIntl(state), i18n: getIntl(state),
isAdmin, isAdmin,
memberships, ...getGroupMemberships(conversation, conversationSelector),
}; };
}; };

View file

@ -4,8 +4,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { import {
GroupV2PendingMembership,
GroupV2RequestingMembership,
PendingInvites, PendingInvites,
PropsType, PropsType,
} from '../../components/conversation/conversation-details/PendingInvites'; } from '../../components/conversation/conversation-details/PendingInvites';
@ -13,7 +11,7 @@ import { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations'; import { getConversationByIdSelector } from '../selectors/conversations';
import { isConversationUnregistered } from '../../util/isConversationUnregistered'; import { getGroupMemberships } from '../../util/getGroupMemberships';
import { assert } from '../../util/assert'; import { assert } from '../../util/assert';
export type SmartPendingInvitesProps = { export type SmartPendingInvitesProps = {
@ -35,39 +33,11 @@ const mapStateToProps = (
'<SmartPendingInvites> expected a conversation to be found' '<SmartPendingInvites> expected a conversation to be found'
); );
const pendingApprovalMemberships = (
conversation.pendingApprovalMemberships || []
).reduce((result: Array<GroupV2RequestingMembership>, membership) => {
const member = conversationSelector(membership.conversationId);
if (!member || isConversationUnregistered(member)) {
return result;
}
return [...result, { member }];
}, []);
const pendingMemberships = (conversation.pendingMemberships || []).reduce(
(result: Array<GroupV2PendingMembership>, membership) => {
const member = conversationSelector(membership.conversationId);
if (!member || isConversationUnregistered(member)) {
return result;
}
return [
...result,
{
member,
metadata: { addedByUserId: membership.addedByUserId },
},
];
},
[]
);
return { return {
...props, ...props,
...getGroupMemberships(conversation, conversationSelector),
conversation, conversation,
i18n: getIntl(state), i18n: getIntl(state),
pendingApprovalMemberships,
pendingMemberships,
}; };
}; };

View file

@ -0,0 +1,235 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { ConversationType } from '../../state/ducks/conversations';
import { getDefaultConversation } from '../helpers/getDefaultConversation';
import { getGroupMemberships } from '../../util/getGroupMemberships';
describe('getGroupMemberships', () => {
const normalConversation1 = getDefaultConversation();
const normalConversation2 = getDefaultConversation();
const unregisteredConversation = getDefaultConversation({
discoveredUnregisteredAt: Date.now(),
});
function getConversationById(id: string): undefined | ConversationType {
return [
normalConversation1,
normalConversation2,
unregisteredConversation,
].find(conversation => conversation.id === id);
}
describe('memberships', () => {
it('returns an empty array if passed undefined', () => {
const conversation = {};
const result = getGroupMemberships(conversation, getConversationById)
.memberships;
assert.isEmpty(result);
});
it('returns an empty array if passed an empty array', () => {
const conversation = { memberships: [] };
const result = getGroupMemberships(conversation, getConversationById)
.memberships;
assert.isEmpty(result);
});
it("filters out conversation IDs that don't exist", () => {
const conversation = {
memberships: [
{
conversationId: 'garbage',
isAdmin: true,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
.memberships;
assert.isEmpty(result);
});
it('filters out unregistered conversations', () => {
const conversation = {
memberships: [
{
conversationId: unregisteredConversation.id,
isAdmin: true,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
.memberships;
assert.isEmpty(result);
});
it('hydrates memberships', () => {
const conversation = {
memberships: [
{
conversationId: normalConversation2.id,
isAdmin: false,
},
{
conversationId: normalConversation1.id,
isAdmin: true,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
.memberships;
assert.lengthOf(result, 2);
assert.deepEqual(result[0], {
isAdmin: false,
member: normalConversation2,
});
assert.deepEqual(result[1], {
isAdmin: true,
member: normalConversation1,
});
});
});
describe('pendingApprovalMemberships', () => {
it('returns an empty array if passed undefined', () => {
const conversation = {};
const result = getGroupMemberships(conversation, getConversationById)
.pendingApprovalMemberships;
assert.isEmpty(result);
});
it('returns an empty array if passed an empty array', () => {
const conversation = { pendingApprovalMemberships: [] };
const result = getGroupMemberships(conversation, getConversationById)
.pendingApprovalMemberships;
assert.isEmpty(result);
});
it("filters out conversation IDs that don't exist", () => {
const conversation = {
pendingApprovalMemberships: [{ conversationId: 'garbage' }],
};
const result = getGroupMemberships(conversation, getConversationById)
.pendingApprovalMemberships;
assert.isEmpty(result);
});
it('filters out unregistered conversations', () => {
const conversation = {
pendingApprovalMemberships: [
{ conversationId: unregisteredConversation.id },
],
};
const result = getGroupMemberships(conversation, getConversationById)
.pendingApprovalMemberships;
assert.isEmpty(result);
});
it('hydrates pending-approval memberships', () => {
const conversation = {
pendingApprovalMemberships: [
{ conversationId: normalConversation2.id },
{ conversationId: normalConversation1.id },
],
};
const result = getGroupMemberships(conversation, getConversationById)
.pendingApprovalMemberships;
assert.lengthOf(result, 2);
assert.deepEqual(result[0], { member: normalConversation2 });
assert.deepEqual(result[1], { member: normalConversation1 });
});
});
describe('pendingMemberships', () => {
it('returns an empty array if passed undefined', () => {
const conversation = {};
const result = getGroupMemberships(conversation, getConversationById)
.pendingMemberships;
assert.isEmpty(result);
});
it('returns an empty array if passed an empty array', () => {
const conversation = { pendingMemberships: [] };
const result = getGroupMemberships(conversation, getConversationById)
.pendingMemberships;
assert.isEmpty(result);
});
it("filters out conversation IDs that don't exist", () => {
const conversation = {
pendingMemberships: [
{ conversationId: 'garbage', addedByUserId: normalConversation1.id },
],
};
const result = getGroupMemberships(conversation, getConversationById)
.pendingMemberships;
assert.isEmpty(result);
});
it('filters out unregistered conversations', () => {
const conversation = {
pendingMemberships: [
{
conversationId: unregisteredConversation.id,
addedByUserId: normalConversation1.id,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
.pendingMemberships;
assert.isEmpty(result);
});
it('hydrates pending memberships', () => {
const conversation = {
pendingMemberships: [
{ conversationId: normalConversation2.id, addedByUserId: 'abc' },
{ conversationId: normalConversation1.id, addedByUserId: 'xyz' },
],
};
const result = getGroupMemberships(conversation, getConversationById)
.pendingMemberships;
assert.lengthOf(result, 2);
assert.deepEqual(result[0], {
member: normalConversation2,
metadata: { addedByUserId: 'abc' },
});
assert.deepEqual(result[1], {
member: normalConversation1,
metadata: { addedByUserId: 'xyz' },
});
});
});
});

View file

@ -0,0 +1,65 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { GroupV2Membership } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
import {
GroupV2PendingMembership,
GroupV2RequestingMembership,
} from '../components/conversation/conversation-details/PendingInvites';
import { ConversationType } from '../state/ducks/conversations';
import { isConversationUnregistered } from './isConversationUnregistered';
export const getGroupMemberships = (
{
memberships = [],
pendingApprovalMemberships = [],
pendingMemberships = [],
}: Readonly<
Pick<
ConversationType,
'memberships' | 'pendingApprovalMemberships' | 'pendingMemberships'
>
>,
getConversationById: (conversationId: string) => undefined | ConversationType
): {
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: Array<GroupV2RequestingMembership>;
pendingMemberships: Array<GroupV2PendingMembership>;
} => ({
memberships: memberships.reduce(
(result: Array<GroupV2Membership>, membership) => {
const member = getConversationById(membership.conversationId);
if (!member || isConversationUnregistered(member)) {
return result;
}
return [...result, { isAdmin: membership.isAdmin, member }];
},
[]
),
pendingApprovalMemberships: pendingApprovalMemberships.reduce(
(result: Array<GroupV2RequestingMembership>, membership) => {
const member = getConversationById(membership.conversationId);
if (!member || isConversationUnregistered(member)) {
return result;
}
return [...result, { member }];
},
[]
),
pendingMemberships: pendingMemberships.reduce(
(result: Array<GroupV2PendingMembership>, membership) => {
const member = getConversationById(membership.conversationId);
if (!member || isConversationUnregistered(member)) {
return result;
}
return [
...result,
{
member,
metadata: { addedByUserId: membership.addedByUserId },
},
];
},
[]
),
});