mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Group Member Labels: A few tweaks
Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
@@ -56,6 +56,8 @@ function getToast(toastType: ToastType): AnyToast {
|
||||
};
|
||||
case ToastType.CallQualitySurveySuccess:
|
||||
return { toastType: ToastType.CallQualitySurveySuccess };
|
||||
case ToastType.CannotAddMemberLabel:
|
||||
return { toastType: ToastType.CannotAddMemberLabel };
|
||||
case ToastType.CannotEditMessage:
|
||||
return { toastType: ToastType.CannotEditMessage };
|
||||
case ToastType.CannotForwardEmptyMessage:
|
||||
|
||||
@@ -165,6 +165,14 @@ export function renderToast({
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CannotAddMemberLabel) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:ToastManager__CannotAddMemberLabel')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CannotEditMessage) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
||||
@@ -12,6 +12,9 @@ import { HasStories } from '../../types/Stories.std.js';
|
||||
import { ThemeType } from '../../types/Util.std.js';
|
||||
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.js';
|
||||
import { getFakeBadges } from '../../test-helpers/getFakeBadge.std.js';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.js';
|
||||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@@ -123,6 +126,23 @@ AsAdmin.args = {
|
||||
areWeAdmin: true,
|
||||
};
|
||||
|
||||
export const AsAdminViewingAdmin = Template.bind({});
|
||||
AsAdminViewingAdmin.args = {
|
||||
areWeAdmin: true,
|
||||
isAdmin: true,
|
||||
};
|
||||
|
||||
export const AsAdminViewingAdminWithLabel = Template.bind({});
|
||||
AsAdminViewingAdminWithLabel.args = {
|
||||
areWeAdmin: true,
|
||||
isAdmin: true,
|
||||
conversation: {
|
||||
...defaultGroup,
|
||||
accessControlAttributes: ACCESS_ENUM.ADMINISTRATOR,
|
||||
},
|
||||
contactLabelString: 'Contact Label',
|
||||
};
|
||||
|
||||
export const AsAdminWithNoGroupLink = Template.bind({});
|
||||
AsAdminWithNoGroupLink.args = {
|
||||
areWeAdmin: true,
|
||||
|
||||
@@ -36,6 +36,9 @@ import type {
|
||||
ToggleGroupMemberLabelInfoModalType,
|
||||
} from '../../state/ducks/globalModals.preload.js';
|
||||
import { GroupMemberLabel } from './ContactName.dom.js';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.js';
|
||||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
|
||||
const log = createLogger('ContactModal');
|
||||
|
||||
@@ -227,6 +230,35 @@ export function ContactModal({
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
isAdmin &&
|
||||
contactLabelString &&
|
||||
conversation.accessControlAttributes === ACCESS_ENUM.ADMINISTRATOR
|
||||
) {
|
||||
modalNode = (
|
||||
<ConfirmationDialog
|
||||
dialogName="ContactModal.toggleAdmin"
|
||||
actions={[
|
||||
{
|
||||
action: () => toggleAdmin(conversation.id, contact.id),
|
||||
text: isAdmin
|
||||
? i18n('icu:ContactModal--rm-admin')
|
||||
: i18n('icu:ContactModal--make-admin'),
|
||||
style: 'affirmative',
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setSubModalState(SubModalState.None)}
|
||||
title={i18n('icu:ContactModal--rm-admin-info', {
|
||||
contact: contact.title,
|
||||
})}
|
||||
>
|
||||
{i18n('icu:ContactModal--rm-admin--clear-label')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
modalNode = (
|
||||
<ConfirmationDialog
|
||||
dialogName="ContactModal.toggleAdmin"
|
||||
@@ -236,6 +268,7 @@ export function ContactModal({
|
||||
text: isAdmin
|
||||
? i18n('icu:ContactModal--rm-admin')
|
||||
: i18n('icu:ContactModal--make-admin'),
|
||||
style: 'affirmative',
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
|
||||
@@ -491,8 +491,7 @@ export function Quote(props: Props): React.JSX.Element | null {
|
||||
dir="auto"
|
||||
className={classNames(
|
||||
getClassName('__primary__author'),
|
||||
isIncoming ? getClassName('__primary__author--incoming') : null,
|
||||
authorLabel ? getClassName('__primary__author--with-label') : null
|
||||
isIncoming ? getClassName('__primary__author--incoming') : null
|
||||
)}
|
||||
>
|
||||
{author}
|
||||
|
||||
@@ -118,6 +118,7 @@ const createProps = (
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
showToast: action('showToast'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
@@ -163,6 +164,18 @@ export function Basic(): React.JSX.Element {
|
||||
return <ConversationDetails {...props} />;
|
||||
}
|
||||
|
||||
export function MemberLabelsEditDisabled(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <ConversationDetails {...props} isEditMemberLabelEnabled={false} />;
|
||||
}
|
||||
|
||||
export function MemberLabelsCannotBeAdded(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <ConversationDetails {...props} canAddLabel={false} />;
|
||||
}
|
||||
|
||||
export function SystemContact(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
const contact = getDefaultConversation();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button, ButtonIconType, ButtonVariant } from '../../Button.dom.js';
|
||||
import type {
|
||||
@@ -65,6 +66,8 @@ import {
|
||||
} from '../InAnotherCallTooltip.dom.js';
|
||||
import { BadgeSustainerInstructionsDialog } from '../../BadgeSustainerInstructionsDialog.dom.js';
|
||||
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
|
||||
import type { ShowToastAction } from '../../../state/ducks/toast.preload.js';
|
||||
import { ToastType } from '../../../types/Toast.dom.js';
|
||||
|
||||
enum ModalState {
|
||||
AddingGroupMembers,
|
||||
@@ -102,6 +105,7 @@ export type StateProps = {
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingAvatarDownload?: boolean;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
showToast: ShowToastAction;
|
||||
selectedNavTab: NavTab;
|
||||
startAvatarDownload: () => void;
|
||||
theme: ThemeType;
|
||||
@@ -208,6 +212,7 @@ export function ConversationDetails({
|
||||
setMuteExpiration,
|
||||
showContactModal,
|
||||
showConversation,
|
||||
showToast,
|
||||
startAvatarDownload,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
@@ -723,13 +728,20 @@ export function ConversationDetails({
|
||||
)}
|
||||
{isGroup && (
|
||||
<ConversationDetailsMembershipList
|
||||
canAddLabel={canAddLabel}
|
||||
canAddNewMembers={canAddNewMembers}
|
||||
conversationId={conversation.id}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
isEditMemberLabelEnabled={isEditMemberLabelEnabled}
|
||||
memberships={memberships}
|
||||
memberColors={memberColors}
|
||||
showContactModal={showContactModal}
|
||||
showLabelEditor={() => {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupMemberLabelEditor,
|
||||
});
|
||||
}}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
}}
|
||||
@@ -756,20 +768,36 @@ export function ConversationDetails({
|
||||
right={hasGroupLink ? i18n('icu:on') : i18n('icu:off')}
|
||||
/>
|
||||
) : null}
|
||||
{canAddLabel && isEditMemberLabelEnabled ? (
|
||||
{isEditMemberLabelEnabled ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:ConversationDetails--member-label')}
|
||||
icon={IconType.tag}
|
||||
disabled={!canAddLabel}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:ConversationDetails--member-label')}
|
||||
onClick={() =>
|
||||
label={
|
||||
<div
|
||||
className={classNames(
|
||||
!canAddLabel
|
||||
? 'ConversationDetails__MemberLabel--disabled'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{i18n('icu:ConversationDetails--member-label')}
|
||||
</div>
|
||||
}
|
||||
onClick={() => {
|
||||
if (!canAddLabel) {
|
||||
showToast({ toastType: ToastType.CannotAddMemberLabel });
|
||||
return;
|
||||
}
|
||||
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupMemberLabelEditor,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<PanelRow
|
||||
|
||||
+31
-1
@@ -17,6 +17,7 @@ import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges
|
||||
import { PanelRow } from './PanelRow.dom.js';
|
||||
import { PanelSection } from './PanelSection.dom.js';
|
||||
import { GroupMemberLabel } from '../ContactName.dom.js';
|
||||
import { AriaClickable } from '../../../axo/AriaClickable.dom.js';
|
||||
|
||||
export type GroupV2Membership = {
|
||||
isAdmin: boolean;
|
||||
@@ -26,14 +27,17 @@ export type GroupV2Membership = {
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
canAddLabel: boolean;
|
||||
canAddNewMembers: boolean;
|
||||
conversationId: string;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
isEditMemberLabelEnabled: boolean;
|
||||
maxShownMemberCount?: number;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
memberColors: Map<string, string>;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showLabelEditor: () => void;
|
||||
startAddingNewMembers?: () => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
@@ -79,13 +83,16 @@ function sortMemberships(
|
||||
|
||||
export function ConversationDetailsMembershipList({
|
||||
canAddNewMembers,
|
||||
canAddLabel,
|
||||
conversationId,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
isEditMemberLabelEnabled,
|
||||
maxShownMemberCount = 5,
|
||||
memberColors,
|
||||
memberships,
|
||||
showContactModal,
|
||||
showLabelEditor,
|
||||
startAddingNewMembers,
|
||||
theme,
|
||||
}: Props): React.JSX.Element {
|
||||
@@ -141,7 +148,7 @@ export function ConversationDetailsMembershipList({
|
||||
/>
|
||||
</div>
|
||||
{labelString && contactNameColor && (
|
||||
<div className="ConversationDetails-membership-list--member-label">
|
||||
<div className="ConversationDetails-membership-list__member-label">
|
||||
<GroupMemberLabel
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={{
|
||||
@@ -152,6 +159,29 @@ export function ConversationDetailsMembershipList({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{canAddLabel &&
|
||||
isEditMemberLabelEnabled &&
|
||||
member.isMe &&
|
||||
(!labelString || !contactNameColor) && (
|
||||
<AriaClickable.SubWidget>
|
||||
<button
|
||||
className="ConversationDetails-membership-list__member-label-button"
|
||||
type="button"
|
||||
onClick={event => {
|
||||
showLabelEditor();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{i18n(
|
||||
'icu:ConversationDetailsMembershipList--add-member-label'
|
||||
)}
|
||||
</div>
|
||||
<div className="ConversationDetails-membership-list__member-label-button__chevron-icon" />
|
||||
</button>
|
||||
</AriaClickable.SubWidget>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
right={isAdmin ? i18n('icu:GroupV2--admin') : ''}
|
||||
|
||||
@@ -149,7 +149,6 @@ export function GroupLinkManagement({
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:GroupLinkManagement--share')}
|
||||
ref={!isAdmin ? focusRef : undefined}
|
||||
onClick={() => {
|
||||
if (conversation.groupLink) {
|
||||
drop(copyGroupLink(conversation.groupLink));
|
||||
|
||||
@@ -101,7 +101,7 @@ export function GroupMemberLabelEditor({
|
||||
|
||||
const isDirty =
|
||||
labelEmoji !== existingLabelEmoji || labelString !== existingLabelString;
|
||||
const canSave = Boolean(isDirty && labelString);
|
||||
const canSave = isDirty;
|
||||
const spinner = isSaving
|
||||
? {
|
||||
'aria-label': i18n('icu:ConversationDetails--member-label--saving'),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useId } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { bemGenerator } from './util.std.js';
|
||||
import { AriaClickable } from '../../../axo/AriaClickable.dom.js';
|
||||
|
||||
export type Props = {
|
||||
alwaysShowActions?: boolean;
|
||||
@@ -19,55 +20,61 @@ export type Props = {
|
||||
|
||||
const bem = bemGenerator('ConversationDetails-panel-row');
|
||||
|
||||
export const PanelRow = React.forwardRef<HTMLButtonElement, Props>(
|
||||
function PanelRowInner(
|
||||
{
|
||||
alwaysShowActions,
|
||||
className,
|
||||
disabled,
|
||||
icon,
|
||||
label,
|
||||
info,
|
||||
right,
|
||||
actions,
|
||||
onClick,
|
||||
}: Props,
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) {
|
||||
const content = (
|
||||
<>
|
||||
{icon !== undefined ? <div className={bem('icon')}>{icon}</div> : null}
|
||||
<div className={bem('label')}>
|
||||
<div>{label}</div>
|
||||
{info !== undefined ? (
|
||||
<div className={bem('info')}>{info}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{right !== undefined ? (
|
||||
<div className={bem('right')}>{right}</div>
|
||||
) : null}
|
||||
{actions !== undefined ? (
|
||||
<div className={alwaysShowActions ? '' : bem('actions')}>
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
export function PanelRow({
|
||||
alwaysShowActions,
|
||||
className,
|
||||
disabled,
|
||||
icon,
|
||||
label,
|
||||
info,
|
||||
right,
|
||||
actions,
|
||||
onClick,
|
||||
}: Props): React.ReactNode {
|
||||
const labelId = useId();
|
||||
const subWidget =
|
||||
actions !== undefined ? (
|
||||
<div className={alwaysShowActions ? '' : bem('actions')}>{actions}</div>
|
||||
) : null;
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
className={classNames(bem('root', 'button'), className)}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
const content = (
|
||||
<>
|
||||
{icon !== undefined ? <div className={bem('icon')}>{icon}</div> : null}
|
||||
<div className={bem('label')}>
|
||||
<div id={labelId}>{label}</div>
|
||||
{info !== undefined ? <div className={bem('info')}>{info}</div> : null}
|
||||
</div>
|
||||
{right !== undefined ? <div className={bem('right')}>{right}</div> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<AriaClickable.Root
|
||||
className={classNames(bem('root', 'button'), className)}
|
||||
>
|
||||
{!disabled && (
|
||||
<AriaClickable.HiddenTrigger
|
||||
onClick={onClick}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
)}
|
||||
<div className={bem('inner')}>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={classNames(bem('root'), className)}>{content}</div>;
|
||||
{subWidget && (
|
||||
<AriaClickable.SubWidget>{subWidget}</AriaClickable.SubWidget>
|
||||
)}
|
||||
</div>
|
||||
</AriaClickable.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(bem('root'), className)}>
|
||||
<div className={bem('inner')}>
|
||||
{content}
|
||||
{subWidget}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user