diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c88fdc28b9..7d61d0aa8b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -4755,6 +4755,10 @@ "message": "Block group", "description": "This is a button to block a group" }, + "ConversationDetailsActions--leave-group-must-choose-new-admin": { + "message": "Before you leave, you must choose at least one new admin for this group.", + "description": "Shown if, before leaving a group, you need to choose an admin" + }, "ConversationDetailsActions--leave-group-modal-title": { "message": "Do you really want to leave?", "description": "This is the modal title for confirming leaving a group" diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 24cc5dd095..38587ebc99 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2966,6 +2966,16 @@ button.module-conversation-details__action-button { &__leave-group { color: $color-accent-red; + + &--disabled { + @include light-theme { + color: $color-gray-60; + } + + @include dark-theme { + color: $color-gray-25; + } + } } &__block-group { @@ -3180,6 +3190,16 @@ button.module-conversation-details__action-button { -webkit-mask: url(../images/icons/v2/leave-24.svg) no-repeat center; background-color: $color-accent-red; } + + &--disabled::after { + @include light-theme { + background-color: $color-gray-60; + } + + @include dark-theme { + background-color: $color-gray-25; + } + } } &--block { @@ -3248,19 +3268,19 @@ button.module-conversation-details__action-button { &--button { color: inherit; background: none; - } - &:hover { - @include light-theme { - background-color: $color-gray-02; - } + &:hover:not(:disabled) { + @include light-theme { + background-color: $color-gray-02; + } - @include dark-theme { - background-color: $color-gray-90; - } + @include dark-theme { + background-color: $color-gray-90; + } - & .module-conversation-details-panel-row__actions { - opacity: 1; + & .module-conversation-details-panel-row__actions { + opacity: 1; + } } } diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx index 7d5327fc8d..76a66a3b8e 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx @@ -25,7 +25,7 @@ const conversation: ConversationType = { lastUpdated: 0, markedUnread: false, memberships: Array.from(Array(32)).map((_, i) => ({ - isAdmin: false, + isAdmin: i === 1, member: getDefaultConversation({ isMe: i === 2, }), @@ -84,6 +84,44 @@ story.add('as Admin', () => { return ; }); +story.add('as last admin', () => { + const props = createProps(); + + return ( + ({ + ...membership, + isAdmin: Boolean(membership.member.isMe), + })), + }} + /> + ); +}); + +story.add('as only admin', () => { + const props = createProps(); + + return ( + membership.member.isMe) + .map(membership => ({ + ...membership, + isAdmin: true, + })), + }} + /> + ); +}); + story.add('Group Editable', () => { const props = createProps(); diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index 6acc62bee2..3f39dfedcb 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -101,12 +101,21 @@ export const ConversationDetails: React.ComponentType = ({ throw new Error('ConversationDetails rendered without a conversation'); } + const memberships = conversation.memberships || []; const pendingMemberships = conversation.pendingMemberships || []; const pendingApprovalMemberships = conversation.pendingApprovalMemberships || []; const invitesCount = pendingMemberships.length + pendingApprovalMemberships.length; + const otherMemberships = memberships.filter(({ member }) => !member.isMe); + const isJustMe = otherMemberships.length === 0; + const isAnyoneElseAnAdmin = otherMemberships.some( + membership => membership.isAdmin + ); + const cannotLeaveBecauseYouAreLastAdmin = + isAdmin && !isJustMe && !isAnyoneElseAnAdmin; + let modalNode: ReactNode; switch (modalState) { case ModalState.NothingOpen: @@ -158,11 +167,7 @@ export const ConversationDetails: React.ComponentType = ({ }); }} conversationIdsAlreadyInGroup={ - new Set( - (conversation.memberships || []).map( - membership => membership.member.id - ) - ) + new Set(memberships.map(membership => membership.member.id)) } groupTitle={conversation.title} i18n={i18n} @@ -238,7 +243,7 @@ export const ConversationDetails: React.ComponentType = ({ { setModalState(ModalState.AddingGroupMembers); @@ -294,6 +299,7 @@ export const ConversationDetails: React.ComponentType = ({ = {}): Props => ({ + cannotLeaveBecauseYouAreLastAdmin: isBoolean( + overrideProps.cannotLeaveBecauseYouAreLastAdmin + ) + ? overrideProps.cannotLeaveBecauseYouAreLastAdmin + : false, conversationTitle: overrideProps.conversationTitle || '', onBlockAndDelete: action('onBlockAndDelete'), onDelete: action('onDelete'), @@ -32,3 +38,9 @@ story.add('Basic', () => { return ; }); + +story.add('Cannot leave because you are the last admin', () => { + const props = createProps({ cannotLeaveBecauseYouAreLastAdmin: true }); + + return ; +}); diff --git a/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx b/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx index 707682a2cf..d60a67c2a9 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx @@ -1,16 +1,19 @@ -// Copyright 2020 Signal Messenger, LLC +// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; +import classNames from 'classnames'; import { LocalizerType } from '../../../types/Util'; import { ConfirmationModal } from '../../ConfirmationModal'; +import { Tooltip, TooltipPlacement } from '../../Tooltip'; import { PanelRow } from './PanelRow'; import { PanelSection } from './PanelSection'; import { ConversationDetailsIcon } from './ConversationDetailsIcon'; export type Props = { + cannotLeaveBecauseYouAreLastAdmin: boolean; conversationTitle: string; onBlockAndDelete: () => void; onDelete: () => void; @@ -18,6 +21,7 @@ export type Props = { }; export const ConversationDetailsActions: React.ComponentType = ({ + cannotLeaveBecauseYouAreLastAdmin, conversationTitle, onBlockAndDelete, onDelete, @@ -26,23 +30,47 @@ export const ConversationDetailsActions: React.ComponentType = ({ const [confirmingLeave, setConfirmingLeave] = React.useState(false); const [confirmingBlock, setConfirmingBlock] = React.useState(false); + let leaveGroupNode = ( + setConfirmingLeave(true)} + icon={ + + } + label={ +
+ {i18n('ConversationDetailsActions--leave-group')} +
+ } + /> + ); + if (cannotLeaveBecauseYouAreLastAdmin) { + leaveGroupNode = ( + + {leaveGroupNode} + + ); + } + return ( <> - setConfirmingLeave(true)} - icon={ - - } - label={ -
- {i18n('ConversationDetailsActions--leave-group')} -
- } - /> + {leaveGroupNode} setConfirmingBlock(true)} icon={ diff --git a/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx b/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx index 4f47818a1a..9feef009a7 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx @@ -1,12 +1,14 @@ -// Copyright 2020 Signal Messenger, LLC +// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; +import classNames from 'classnames'; import { bemGenerator } from './util'; export type Props = { ariaLabel: string; + disabled?: boolean; icon: string; onClick?: () => void; }; @@ -15,16 +17,26 @@ const bem = bemGenerator('module-conversation-details-icon'); export const ConversationDetailsIcon: React.ComponentType = ({ ariaLabel, + disabled, icon, onClick, }) => { - const content =
; + const iconClassName = bem('icon', icon); + const content = ( +
+ ); if (onClick) { return (