Don't let users leave groups if they're the only admin
This commit is contained in:
parent
a7c78b3b23
commit
c8dc8a7398
8 changed files with 158 additions and 35 deletions
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <ConversationDetails {...props} isAdmin />;
|
||||
});
|
||||
|
||||
story.add('as last admin', () => {
|
||||
const props = createProps();
|
||||
|
||||
return (
|
||||
<ConversationDetails
|
||||
{...props}
|
||||
isAdmin
|
||||
conversation={{
|
||||
...conversation,
|
||||
memberships: conversation.memberships?.map(membership => ({
|
||||
...membership,
|
||||
isAdmin: Boolean(membership.member.isMe),
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('as only admin', () => {
|
||||
const props = createProps();
|
||||
|
||||
return (
|
||||
<ConversationDetails
|
||||
{...props}
|
||||
isAdmin
|
||||
conversation={{
|
||||
...conversation,
|
||||
memberships: conversation.memberships
|
||||
?.filter(membership => membership.member.isMe)
|
||||
.map(membership => ({
|
||||
...membership,
|
||||
isAdmin: true,
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Group Editable', () => {
|
||||
const props = createProps();
|
||||
|
||||
|
|
|
@ -101,12 +101,21 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
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<Props> = ({
|
|||
});
|
||||
}}
|
||||
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<Props> = ({
|
|||
<ConversationDetailsMembershipList
|
||||
canAddNewMembers={canEditGroupInfo}
|
||||
i18n={i18n}
|
||||
memberships={conversation.memberships || []}
|
||||
memberships={memberships}
|
||||
showContactModal={showContactModal}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
|
@ -294,6 +299,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
|
||||
<ConversationDetailsActions
|
||||
i18n={i18n}
|
||||
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
||||
conversationTitle={conversation.title}
|
||||
onDelete={onDelete}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { isBoolean } from 'lodash';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
@ -21,6 +22,11 @@ const story = storiesOf(
|
|||
);
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): 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 <ConversationDetailsActions {...props} />;
|
||||
});
|
||||
|
||||
story.add('Cannot leave because you are the last admin', () => {
|
||||
const props = createProps({ cannotLeaveBecauseYouAreLastAdmin: true });
|
||||
|
||||
return <ConversationDetailsActions {...props} />;
|
||||
});
|
||||
|
|
|
@ -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<Props> = ({
|
||||
cannotLeaveBecauseYouAreLastAdmin,
|
||||
conversationTitle,
|
||||
onBlockAndDelete,
|
||||
onDelete,
|
||||
|
@ -26,23 +30,47 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
|||
const [confirmingLeave, setConfirmingLeave] = React.useState<boolean>(false);
|
||||
const [confirmingBlock, setConfirmingBlock] = React.useState<boolean>(false);
|
||||
|
||||
let leaveGroupNode = (
|
||||
<PanelRow
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
onClick={() => setConfirmingLeave(true)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsActions--leave-group')}
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
icon="leave"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div
|
||||
className={classNames(
|
||||
'module-conversation-details__leave-group',
|
||||
cannotLeaveBecauseYouAreLastAdmin &&
|
||||
'module-conversation-details__leave-group--disabled'
|
||||
)}
|
||||
>
|
||||
{i18n('ConversationDetailsActions--leave-group')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
leaveGroupNode = (
|
||||
<Tooltip
|
||||
content={i18n(
|
||||
'ConversationDetailsActions--leave-group-must-choose-new-admin'
|
||||
)}
|
||||
direction={TooltipPlacement.Top}
|
||||
>
|
||||
{leaveGroupNode}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelSection>
|
||||
<PanelRow
|
||||
onClick={() => setConfirmingLeave(true)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsActions--leave-group')}
|
||||
icon="leave"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div className="module-conversation-details__leave-group">
|
||||
{i18n('ConversationDetailsActions--leave-group')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{leaveGroupNode}
|
||||
<PanelRow
|
||||
onClick={() => setConfirmingBlock(true)}
|
||||
icon={
|
||||
|
|
|
@ -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<Props> = ({
|
||||
ariaLabel,
|
||||
disabled,
|
||||
icon,
|
||||
onClick,
|
||||
}) => {
|
||||
const content = <div className={bem('icon', icon)} />;
|
||||
const iconClassName = bem('icon', icon);
|
||||
const content = (
|
||||
<div
|
||||
className={classNames(
|
||||
iconClassName,
|
||||
disabled && `${iconClassName}--disabled`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
aria-label={ariaLabel}
|
||||
className={bem('button')}
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { bemGenerator } from './util';
|
|||
export type Props = {
|
||||
alwaysShowActions?: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
label: string | React.ReactNode;
|
||||
info?: string;
|
||||
|
@ -21,6 +22,7 @@ const bem = bemGenerator('module-conversation-details-panel-row');
|
|||
export const PanelRow: React.ComponentType<Props> = ({
|
||||
alwaysShowActions,
|
||||
className,
|
||||
disabled,
|
||||
icon,
|
||||
label,
|
||||
info,
|
||||
|
@ -45,6 +47,7 @@ export const PanelRow: React.ComponentType<Props> = ({
|
|||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
className={classNames(bem('root', 'button'), className)}
|
||||
onClick={onClick}
|
||||
|
|
Loading…
Reference in a new issue