Improvements to Group Settings screen

This commit is contained in:
Josh Perez 2021-03-02 11:27:11 -05:00 committed by Josh Perez
parent dfa5005e7d
commit 12bba24dbd
11 changed files with 146 additions and 79 deletions

View File

@ -3390,6 +3390,10 @@
"message": "Admin",
"description": "Label for a group administrator"
},
"GroupV2--only-admins": {
"message": "Only Admins",
"description": "Label for group administrators -- used in drop-downs to select permissions that apply to admins"
},
"GroupV2--all-members": {
"message": "All members",
"description": "Label for describing the general non-privileged members of a group"

View File

@ -23,9 +23,11 @@ const conversation: ConversationType = {
id: '',
lastUpdated: 0,
markedUnread: false,
memberships: Array.from(Array(32)).map(() => ({
memberships: Array.from(Array(32)).map((_, i) => ({
isAdmin: false,
member: getDefaultConversation({}),
member: getDefaultConversation({
isMe: i === 2,
}),
metadata: {
conversationId: '',
joinedAtVersion: 0,

View File

@ -119,7 +119,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
/>
<PanelSection>
{isAdmin ? (
{isAdmin || hasGroupLink ? (
<PanelRow
icon={
<ConversationDetailsIcon

View File

@ -34,15 +34,17 @@ const createMemberships = (
isAdmin: i % 3 === 0,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata: {} as any,
member: getDefaultConversation({}),
member: getDefaultConversation({
isMe: i === 2,
}),
})
);
};
const createProps = (overrideProps: Partial<Props>): Props => ({
i18n,
showContactModal: action('showContactModal'),
memberships: overrideProps.memberships || [],
showContactModal: action('showContactModal'),
});
story.add('Few', () => {

View File

@ -26,26 +26,66 @@ export type Props = {
const MAX_MEMBER_COUNT = 5;
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
function sortConversationTitles(
left: GroupV2Membership,
right: GroupV2Membership
) {
const leftTitle = left.member.title;
const rightTitle = right.member.title;
return collator.compare(leftTitle, rightTitle);
}
function sortMemberships(
memberships: ReadonlyArray<GroupV2Membership>
): Array<GroupV2Membership> {
let you: undefined | GroupV2Membership;
const admins: Array<GroupV2Membership> = [];
const nonAdmins: Array<GroupV2Membership> = [];
memberships.forEach(membershipInfo => {
const { isAdmin, member } = membershipInfo;
if (member.isMe) {
you = membershipInfo;
} else if (isAdmin) {
admins.push(membershipInfo);
} else {
nonAdmins.push(membershipInfo);
}
});
admins.sort(sortConversationTitles);
nonAdmins.sort(sortConversationTitles);
const sortedMemberships = [];
if (you) {
sortedMemberships.push(you);
}
sortedMemberships.push(...admins);
sortedMemberships.push(...nonAdmins);
return sortedMemberships;
}
export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
memberships,
showContactModal,
i18n,
}) => {
const [showAllMembers, setShowAllMembers] = React.useState<boolean>(false);
const sortedMemberships = sortMemberships(memberships);
const shouldHideRestMembers = memberships.length - MAX_MEMBER_COUNT > 1;
const shouldHideRestMembers = sortedMemberships.length - MAX_MEMBER_COUNT > 1;
const membersToShow =
shouldHideRestMembers && !showAllMembers
? MAX_MEMBER_COUNT
: memberships.length;
: sortedMemberships.length;
return (
<PanelSection
title={i18n('ConversationDetailsMembershipList--title', [
memberships.length.toString(),
sortedMemberships.length.toString(),
])}
>
{memberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
<PanelRow
key={member.id}
onClick={() => showContactModal(member.id)}

View File

@ -51,36 +51,50 @@ function getConversation(
};
}
const createProps = (conversation?: ConversationType): PropsType => ({
const createProps = (
conversation?: ConversationType,
isAdmin = false
): PropsType => ({
accessEnum: AccessEnum,
changeHasGroupLink: action('changeHasGroupLink'),
conversation: conversation || getConversation(),
copyGroupLink: action('copyGroupLink'),
generateNewGroupLink: action('generateNewGroupLink'),
i18n,
isAdmin,
setAccessControlAddFromInviteLinkSetting: action(
'setAccessControlAddFromInviteLinkSetting'
),
});
story.add('Off', () => {
const props = createProps();
story.add('Off (Admin)', () => {
const props = createProps(undefined, true);
return <GroupLinkManagement {...props} />;
});
story.add('On', () => {
story.add('On (Admin)', () => {
const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ANY),
true
);
return <GroupLinkManagement {...props} />;
});
story.add('On (Admin + Admin Approval Needed)', () => {
const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR),
true
);
return <GroupLinkManagement {...props} />;
});
story.add('On (Non-admin)', () => {
const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ANY)
);
return <GroupLinkManagement {...props} />;
});
story.add('On (Admin Approval Needed)', () => {
const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR)
);
return <GroupLinkManagement {...props} />;
});

View File

@ -17,6 +17,7 @@ export type PropsType = {
copyGroupLink: (groupLink: string) => void;
generateNewGroupLink: () => void;
i18n: LocalizerType;
isAdmin: boolean;
setAccessControlAddFromInviteLinkSetting: (value: boolean) => void;
};
@ -27,6 +28,7 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
copyGroupLink,
generateNewGroupLink,
i18n,
isAdmin,
setAccessControlAddFromInviteLinkSetting,
}) => {
if (conversation === undefined) {
@ -54,19 +56,21 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
info={groupLinkInfo}
label={i18n('ConversationDetails--group-link')}
right={
<div className="module-conversation-details-select">
<select
onChange={createEventHandler(changeHasGroupLink)}
value={String(Boolean(hasGroupLink))}
>
<option value="true" aria-label={i18n('on')}>
{i18n('on')}
</option>
<option value="false" aria-label={i18n('off')}>
{i18n('off')}
</option>
</select>
</div>
isAdmin ? (
<div className="module-conversation-details-select">
<select
onChange={createEventHandler(changeHasGroupLink)}
value={String(Boolean(hasGroupLink))}
>
<option value="true" aria-label={i18n('on')}>
{i18n('on')}
</option>
<option value="false" aria-label={i18n('off')}>
{i18n('off')}
</option>
</select>
</div>
) : null
}
/>
</PanelSection>
@ -88,41 +92,45 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
}
}}
/>
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('GroupLinkManagement--reset')}
icon="reset"
/>
}
label={i18n('GroupLinkManagement--reset')}
onClick={generateNewGroupLink}
/>
{isAdmin ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('GroupLinkManagement--reset')}
icon="reset"
/>
}
label={i18n('GroupLinkManagement--reset')}
onClick={generateNewGroupLink}
/>
) : null}
</PanelSection>
<PanelSection>
<PanelRow
info={i18n('GroupLinkManagement--approve-info')}
label={i18n('GroupLinkManagement--approve-label')}
right={
<div className="module-conversation-details-select">
<select
onChange={createEventHandler(
setAccessControlAddFromInviteLinkSetting
)}
value={String(membersNeedAdminApproval)}
>
<option value="true" aria-label={i18n('on')}>
{i18n('on')}
</option>
<option value="false" aria-label={i18n('off')}>
{i18n('off')}
</option>
</select>
</div>
}
/>
</PanelSection>
{isAdmin ? (
<PanelSection>
<PanelRow
info={i18n('GroupLinkManagement--approve-info')}
label={i18n('GroupLinkManagement--approve-label')}
right={
<div className="module-conversation-details-select">
<select
onChange={createEventHandler(
setAccessControlAddFromInviteLinkSetting
)}
value={String(membersNeedAdminApproval)}
>
<option value="true" aria-label={i18n('on')}>
{i18n('on')}
</option>
<option value="false" aria-label={i18n('off')}>
{i18n('off')}
</option>
</select>
</div>
}
/>
</PanelSection>
) : null}
</>
) : null}
</>

View File

@ -135,7 +135,7 @@ export class ConversationModel extends window.Backbone.Model<
verifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus;
intlCollator = new Intl.Collator();
intlCollator = new Intl.Collator(undefined, { sensitivity: 'base' });
private cachedLatestGroupCallEraId?: string;
@ -183,14 +183,12 @@ export class ConversationModel extends window.Backbone.Model<
// eslint-disable-next-line class-methods-use-this
getContactCollection(): Backbone.Collection<ConversationModel> {
const collection = new window.Backbone.Collection<ConversationModel>();
const collator = new Intl.Collator();
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
collection.comparator = (
left: ConversationModel,
right: ConversationModel
) => {
const leftLower = left.getTitle().toLowerCase();
const rightLower = right.getTitle().toLowerCase();
return collator.compare(leftLower, rightLower);
return collator.compare(left.getTitle(), right.getTitle());
};
return collection;
}
@ -5229,9 +5227,7 @@ const sortConversationTitles = (
right: SortableByTitle,
collator: Intl.Collator
) => {
const leftLower = left.getTitle().toLowerCase();
const rightLower = right.getTitle().toLowerCase();
return collator.compare(leftLower, rightLower);
return collator.compare(left.getTitle(), right.getTitle());
};
// We need a custom collection here to get the sorting we need
@ -5239,7 +5235,7 @@ window.Whisper.GroupConversationCollection = window.Backbone.Collection.extend({
model: window.Whisper.GroupMemberConversation,
initialize() {
this.collator = new Intl.Collator();
this.collator = new Intl.Collator(undefined, { sensitivity: 'base' });
},
comparator(left: WhatIsThis, right: WhatIsThis) {

View File

@ -39,8 +39,7 @@ const mapStateToProps = (
conversation && conversation.canEditGroupInfo
? conversation.canEditGroupInfo
: false;
const isAdmin =
conversation && conversation.areWeAdmin ? conversation.areWeAdmin : false;
const isAdmin = Boolean(conversation?.areWeAdmin);
return {
...props,

View File

@ -26,11 +26,13 @@ const mapStateToProps = (
props: SmartGroupLinkManagementProps
): PropsType => {
const conversation = getConversationSelector(state)(props.conversationId);
const isAdmin = Boolean(conversation?.areWeAdmin);
return {
...props,
conversation,
i18n: getIntl(state),
isAdmin,
};
};

View File

@ -19,7 +19,7 @@ export function getAccessControlOptions(
value: accessEnum.MEMBER,
},
{
name: i18n('GroupV2--admin'),
name: i18n('GroupV2--only-admins'),
value: accessEnum.ADMINISTRATOR,
},
];