diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 16b1373fe5..e5b9432d7f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx index 3b6b8fe261..99a816eb1f 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx @@ -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, diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index b6338c2c59..7a5897b23a 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -119,7 +119,7 @@ export const ConversationDetails: React.ComponentType = ({ /> - {isAdmin ? ( + {isAdmin || hasGroupLink ? ( ): Props => ({ i18n, - showContactModal: action('showContactModal'), memberships: overrideProps.memberships || [], + showContactModal: action('showContactModal'), }); story.add('Few', () => { diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx index 01c191040e..647e5df3d3 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx @@ -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 +): Array { + let you: undefined | GroupV2Membership; + const admins: Array = []; + const nonAdmins: Array = []; + 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 = ({ memberships, showContactModal, i18n, }) => { const [showAllMembers, setShowAllMembers] = React.useState(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 ( - {memberships.slice(0, membersToShow).map(({ isAdmin, member }) => ( + {sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => ( showContactModal(member.id)} diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx index 5a75f63dad..31647fdcdf 100644 --- a/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx +++ b/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx @@ -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 ; }); -story.add('On', () => { +story.add('On (Admin)', () => { + const props = createProps( + getConversation('https://signal.group/1', AccessEnum.ANY), + true + ); + + return ; +}); + +story.add('On (Admin + Admin Approval Needed)', () => { + const props = createProps( + getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR), + true + ); + + return ; +}); + +story.add('On (Non-admin)', () => { const props = createProps( getConversation('https://signal.group/1', AccessEnum.ANY) ); return ; }); - -story.add('On (Admin Approval Needed)', () => { - const props = createProps( - getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR) - ); - - return ; -}); diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx index a3927689d8..de1e65188b 100644 --- a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx +++ b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx @@ -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 = ({ copyGroupLink, generateNewGroupLink, i18n, + isAdmin, setAccessControlAddFromInviteLinkSetting, }) => { if (conversation === undefined) { @@ -54,19 +56,21 @@ export const GroupLinkManagement: React.ComponentType = ({ info={groupLinkInfo} label={i18n('ConversationDetails--group-link')} right={ -
- -
+ isAdmin ? ( +
+ +
+ ) : null } />
@@ -88,41 +92,45 @@ export const GroupLinkManagement: React.ComponentType = ({ } }} /> - - } - label={i18n('GroupLinkManagement--reset')} - onClick={generateNewGroupLink} - /> + {isAdmin ? ( + + } + label={i18n('GroupLinkManagement--reset')} + onClick={generateNewGroupLink} + /> + ) : null}
- - - - - } - /> - + {isAdmin ? ( + + + + + } + /> + + ) : null} ) : null} diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index a378a1c8f2..14037650c7 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -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 { const collection = new window.Backbone.Collection(); - 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) { diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx index cfd632ad58..12d6012c0a 100644 --- a/ts/state/smart/ConversationDetails.tsx +++ b/ts/state/smart/ConversationDetails.tsx @@ -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, diff --git a/ts/state/smart/GroupLinkManagement.tsx b/ts/state/smart/GroupLinkManagement.tsx index d8d6e7ad0c..b32bd1cac8 100644 --- a/ts/state/smart/GroupLinkManagement.tsx +++ b/ts/state/smart/GroupLinkManagement.tsx @@ -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, }; }; diff --git a/ts/util/getAccessControlOptions.ts b/ts/util/getAccessControlOptions.ts index 97fe8cb31e..ac7c3d0686 100644 --- a/ts/util/getAccessControlOptions.ts +++ b/ts/util/getAccessControlOptions.ts @@ -19,7 +19,7 @@ export function getAccessControlOptions( value: accessEnum.MEMBER, }, { - name: i18n('GroupV2--admin'), + name: i18n('GroupV2--only-admins'), value: accessEnum.ADMINISTRATOR, }, ];