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", "message": "Admin",
"description": "Label for a group administrator" "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": { "GroupV2--all-members": {
"message": "All members", "message": "All members",
"description": "Label for describing the general non-privileged members of a group" "description": "Label for describing the general non-privileged members of a group"

View file

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

View file

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

View file

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

View file

@ -26,26 +26,66 @@ export type Props = {
const MAX_MEMBER_COUNT = 5; 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> = ({ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
memberships, memberships,
showContactModal, showContactModal,
i18n, i18n,
}) => { }) => {
const [showAllMembers, setShowAllMembers] = React.useState<boolean>(false); 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 = const membersToShow =
shouldHideRestMembers && !showAllMembers shouldHideRestMembers && !showAllMembers
? MAX_MEMBER_COUNT ? MAX_MEMBER_COUNT
: memberships.length; : sortedMemberships.length;
return ( return (
<PanelSection <PanelSection
title={i18n('ConversationDetailsMembershipList--title', [ 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 <PanelRow
key={member.id} key={member.id}
onClick={() => showContactModal(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, accessEnum: AccessEnum,
changeHasGroupLink: action('changeHasGroupLink'), changeHasGroupLink: action('changeHasGroupLink'),
conversation: conversation || getConversation(), conversation: conversation || getConversation(),
copyGroupLink: action('copyGroupLink'), copyGroupLink: action('copyGroupLink'),
generateNewGroupLink: action('generateNewGroupLink'), generateNewGroupLink: action('generateNewGroupLink'),
i18n, i18n,
isAdmin,
setAccessControlAddFromInviteLinkSetting: action( setAccessControlAddFromInviteLinkSetting: action(
'setAccessControlAddFromInviteLinkSetting' 'setAccessControlAddFromInviteLinkSetting'
), ),
}); });
story.add('Off', () => { story.add('Off (Admin)', () => {
const props = createProps(); const props = createProps(undefined, true);
return <GroupLinkManagement {...props} />; 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( const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ANY) getConversation('https://signal.group/1', AccessEnum.ANY)
); );
return <GroupLinkManagement {...props} />; 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; copyGroupLink: (groupLink: string) => void;
generateNewGroupLink: () => void; generateNewGroupLink: () => void;
i18n: LocalizerType; i18n: LocalizerType;
isAdmin: boolean;
setAccessControlAddFromInviteLinkSetting: (value: boolean) => void; setAccessControlAddFromInviteLinkSetting: (value: boolean) => void;
}; };
@ -27,6 +28,7 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
copyGroupLink, copyGroupLink,
generateNewGroupLink, generateNewGroupLink,
i18n, i18n,
isAdmin,
setAccessControlAddFromInviteLinkSetting, setAccessControlAddFromInviteLinkSetting,
}) => { }) => {
if (conversation === undefined) { if (conversation === undefined) {
@ -54,19 +56,21 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
info={groupLinkInfo} info={groupLinkInfo}
label={i18n('ConversationDetails--group-link')} label={i18n('ConversationDetails--group-link')}
right={ right={
<div className="module-conversation-details-select"> isAdmin ? (
<select <div className="module-conversation-details-select">
onChange={createEventHandler(changeHasGroupLink)} <select
value={String(Boolean(hasGroupLink))} onChange={createEventHandler(changeHasGroupLink)}
> value={String(Boolean(hasGroupLink))}
<option value="true" aria-label={i18n('on')}> >
{i18n('on')} <option value="true" aria-label={i18n('on')}>
</option> {i18n('on')}
<option value="false" aria-label={i18n('off')}> </option>
{i18n('off')} <option value="false" aria-label={i18n('off')}>
</option> {i18n('off')}
</select> </option>
</div> </select>
</div>
) : null
} }
/> />
</PanelSection> </PanelSection>
@ -88,41 +92,45 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
} }
}} }}
/> />
<PanelRow {isAdmin ? (
icon={ <PanelRow
<ConversationDetailsIcon icon={
ariaLabel={i18n('GroupLinkManagement--reset')} <ConversationDetailsIcon
icon="reset" ariaLabel={i18n('GroupLinkManagement--reset')}
/> icon="reset"
} />
label={i18n('GroupLinkManagement--reset')} }
onClick={generateNewGroupLink} label={i18n('GroupLinkManagement--reset')}
/> onClick={generateNewGroupLink}
/>
) : null}
</PanelSection> </PanelSection>
<PanelSection> {isAdmin ? (
<PanelRow <PanelSection>
info={i18n('GroupLinkManagement--approve-info')} <PanelRow
label={i18n('GroupLinkManagement--approve-label')} info={i18n('GroupLinkManagement--approve-info')}
right={ label={i18n('GroupLinkManagement--approve-label')}
<div className="module-conversation-details-select"> right={
<select <div className="module-conversation-details-select">
onChange={createEventHandler( <select
setAccessControlAddFromInviteLinkSetting onChange={createEventHandler(
)} setAccessControlAddFromInviteLinkSetting
value={String(membersNeedAdminApproval)} )}
> value={String(membersNeedAdminApproval)}
<option value="true" aria-label={i18n('on')}> >
{i18n('on')} <option value="true" aria-label={i18n('on')}>
</option> {i18n('on')}
<option value="false" aria-label={i18n('off')}> </option>
{i18n('off')} <option value="false" aria-label={i18n('off')}>
</option> {i18n('off')}
</select> </option>
</div> </select>
} </div>
/> }
</PanelSection> />
</PanelSection>
) : null}
</> </>
) : null} ) : null}
</> </>

View file

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

View file

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

View file

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

View file

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