Improvements to Group Settings screen
This commit is contained in:
parent
dfa5005e7d
commit
12bba24dbd
11 changed files with 146 additions and 79 deletions
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -119,7 +119,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PanelSection>
|
<PanelSection>
|
||||||
{isAdmin ? (
|
{isAdmin || hasGroupLink ? (
|
||||||
<PanelRow
|
<PanelRow
|
||||||
icon={
|
icon={
|
||||||
<ConversationDetailsIcon
|
<ConversationDetailsIcon
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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} />;
|
|
||||||
});
|
|
||||||
|
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Add table
Reference in a new issue