Support for announcement-only groups
This commit is contained in:
parent
863ae9ed83
commit
56d5d283bd
43 changed files with 1057 additions and 455 deletions
67
ts/components/AnnouncementsOnlyGroupBanner.tsx
Normal file
67
ts/components/AnnouncementsOnlyGroupBanner.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
import { ConversationListItem } from './conversationList/ConversationListItem';
|
||||
|
||||
type PropsType = {
|
||||
groupAdmins: Array<ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
openConversation: (conversationId: string) => unknown;
|
||||
};
|
||||
|
||||
export const AnnouncementsOnlyGroupBanner = ({
|
||||
groupAdmins,
|
||||
i18n,
|
||||
openConversation,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [isShowingAdmins, setIsShowingAdmins] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isShowingAdmins && (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
onClose={() => setIsShowingAdmins(false)}
|
||||
title={i18n('AnnouncementsOnlyGroupBanner--modal')}
|
||||
>
|
||||
{groupAdmins.map(admin => (
|
||||
<ConversationListItem
|
||||
{...admin}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
openConversation(admin.id);
|
||||
}}
|
||||
// Required by the component but unecessary for us
|
||||
style={{}}
|
||||
// We don't want these values to show
|
||||
draftPreview=""
|
||||
lastMessage={undefined}
|
||||
lastUpdated={undefined}
|
||||
typingContact={undefined}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
)}
|
||||
<div className="AnnouncementsOnlyGroupBanner__banner">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="AnnouncementsOnlyGroupBanner--announcements-only"
|
||||
components={[
|
||||
<button
|
||||
className="AnnouncementsOnlyGroupBanner__banner--admins"
|
||||
type="button"
|
||||
onClick={() => setIsShowingAdmins(true)}
|
||||
>
|
||||
{i18n('AnnouncementsOnlyGroupBanner--admins')}
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -91,7 +91,14 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
title: '',
|
||||
// GroupV1 Disabled Actions
|
||||
onStartGroupMigration: action('onStartGroupMigration'),
|
||||
// GroupV2 Pending Approval Actions
|
||||
// GroupV2
|
||||
announcementsOnly: boolean(
|
||||
'announcementsOnly',
|
||||
Boolean(overrideProps.announcementsOnly)
|
||||
),
|
||||
areWeAdmin: boolean('areWeAdmin', Boolean(overrideProps.areWeAdmin)),
|
||||
groupAdmins: [],
|
||||
openConversation: action('openConversation'),
|
||||
onCancelJoinRequest: action('onCancelJoinRequest'),
|
||||
// SMS-only
|
||||
isSMSOnly: overrideProps.isSMSOnly || false,
|
||||
|
@ -157,3 +164,12 @@ story.add('Attachments', () => {
|
|||
|
||||
return <CompositionArea {...props} />;
|
||||
});
|
||||
|
||||
story.add('Announcements Only group', () => (
|
||||
<CompositionArea
|
||||
{...createProps({
|
||||
announcementsOnly: true,
|
||||
areWeAdmin: false,
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -37,11 +37,16 @@ import { MediaQualitySelector } from './MediaQualitySelector';
|
|||
import { Quote, Props as QuoteProps } from './conversation/Quote';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import { LinkPreviewWithDomain } from '../types/LinkPreview';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly areWePending?: boolean;
|
||||
readonly areWePendingApproval?: boolean;
|
||||
readonly announcementsOnly?: boolean;
|
||||
readonly areWeAdmin?: boolean;
|
||||
readonly groupAdmins: Array<ConversationType>;
|
||||
readonly groupVersion?: 1 | 2;
|
||||
readonly isGroupV1AndDisabled?: boolean;
|
||||
readonly isMissingMandatoryProfileSharing?: boolean;
|
||||
|
@ -74,6 +79,7 @@ export type OwnProps = {
|
|||
linkPreviewLoading: boolean;
|
||||
linkPreviewResult?: LinkPreviewWithDomain;
|
||||
onCloseLinkPreview(): unknown;
|
||||
openConversation(conversationId: string): unknown;
|
||||
};
|
||||
|
||||
export type Props = Pick<
|
||||
|
@ -188,8 +194,12 @@ export const CompositionArea = ({
|
|||
// GroupV1 Disabled Actions
|
||||
isGroupV1AndDisabled,
|
||||
onStartGroupMigration,
|
||||
// GroupV2 Pending Approval Actions
|
||||
// GroupV2
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
groupAdmins,
|
||||
onCancelJoinRequest,
|
||||
openConversation,
|
||||
// SMS-only contacts
|
||||
isSMSOnly,
|
||||
isFetchingUUID,
|
||||
|
@ -283,7 +293,7 @@ export const CompositionArea = ({
|
|||
|
||||
const leftHandSideButtonsFragment = (
|
||||
<>
|
||||
<div className="module-composition-area__button-cell">
|
||||
<div className="CompositionArea__button-cell">
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
doSend={handleForceSend}
|
||||
|
@ -295,7 +305,7 @@ export const CompositionArea = ({
|
|||
/>
|
||||
</div>
|
||||
{showMediaQualitySelector ? (
|
||||
<div className="module-composition-area__button-cell">
|
||||
<div className="CompositionArea__button-cell">
|
||||
<MediaQualitySelector
|
||||
i18n={i18n}
|
||||
isHighQuality={shouldSendHighQualityAttachments}
|
||||
|
@ -309,11 +319,11 @@ export const CompositionArea = ({
|
|||
const micButtonFragment = showMic ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-area__button-cell',
|
||||
micActive ? 'module-composition-area__button-cell--mic-active' : null,
|
||||
large ? 'module-composition-area__button-cell--large-right' : null,
|
||||
'CompositionArea__button-cell',
|
||||
micActive ? 'CompositionArea__button-cell--mic-active' : null,
|
||||
large ? 'CompositionArea__button-cell--large-right' : null,
|
||||
micActive && large
|
||||
? 'module-composition-area__button-cell--large-right-mic-active'
|
||||
? 'CompositionArea__button-cell--large-right-mic-active'
|
||||
: null
|
||||
)}
|
||||
ref={micCellRef}
|
||||
|
@ -321,7 +331,7 @@ export const CompositionArea = ({
|
|||
) : null;
|
||||
|
||||
const attButton = (
|
||||
<div className="module-composition-area__button-cell">
|
||||
<div className="CompositionArea__button-cell">
|
||||
<div className="choose-file">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -336,13 +346,13 @@ export const CompositionArea = ({
|
|||
const sendButtonFragment = (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-area__button-cell',
|
||||
large ? 'module-composition-area__button-cell--large-right' : null
|
||||
'CompositionArea__button-cell',
|
||||
large ? 'CompositionArea__button-cell--large-right' : null
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="module-composition-area__send-button"
|
||||
className="CompositionArea__send-button"
|
||||
onClick={handleForceSend}
|
||||
aria-label={i18n('sendMessageToContact')}
|
||||
/>
|
||||
|
@ -351,7 +361,7 @@ export const CompositionArea = ({
|
|||
|
||||
const stickerButtonPlacement = large ? 'top-start' : 'top-end';
|
||||
const stickerButtonFragment = withStickers ? (
|
||||
<div className="module-composition-area__button-cell">
|
||||
<div className="CompositionArea__button-cell">
|
||||
<StickerButton
|
||||
i18n={i18n}
|
||||
knownPacks={knownPacks}
|
||||
|
@ -422,9 +432,9 @@ export const CompositionArea = ({
|
|||
return (
|
||||
<div
|
||||
className={classNames([
|
||||
'module-composition-area',
|
||||
'module-composition-area--sms-only',
|
||||
isFetchingUUID ? 'module-composition-area--pending' : null,
|
||||
'CompositionArea',
|
||||
'CompositionArea--sms-only',
|
||||
isFetchingUUID ? 'CompositionArea--pending' : null,
|
||||
])}
|
||||
>
|
||||
{isFetchingUUID ? (
|
||||
|
@ -436,10 +446,10 @@ export const CompositionArea = ({
|
|||
/>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="module-composition-area--sms-only__title">
|
||||
<h2 className="CompositionArea--sms-only__title">
|
||||
{i18n('CompositionArea--sms-only__title')}
|
||||
</h2>
|
||||
<p className="module-composition-area--sms-only__body">
|
||||
<p className="CompositionArea--sms-only__body">
|
||||
{i18n('CompositionArea--sms-only__body')}
|
||||
</p>
|
||||
</>
|
||||
|
@ -490,16 +500,24 @@ export const CompositionArea = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (announcementsOnly && !areWeAdmin) {
|
||||
return (
|
||||
<AnnouncementsOnlyGroupBanner
|
||||
groupAdmins={groupAdmins}
|
||||
i18n={i18n}
|
||||
openConversation={openConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-composition-area">
|
||||
<div className="module-composition-area__toggle-large">
|
||||
<div className="CompositionArea">
|
||||
<div className="CompositionArea__toggle-large">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-composition-area__toggle-large__button',
|
||||
large
|
||||
? 'module-composition-area__toggle-large__button--large-active'
|
||||
: null
|
||||
'CompositionArea__toggle-large__button',
|
||||
large ? 'CompositionArea__toggle-large__button--large-active' : null
|
||||
)}
|
||||
// This prevents the user from tabbing here
|
||||
tabIndex={-1}
|
||||
|
@ -509,8 +527,8 @@ export const CompositionArea = ({
|
|||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-area__row',
|
||||
'module-composition-area__row--column'
|
||||
'CompositionArea__row',
|
||||
'CompositionArea__row--column'
|
||||
)}
|
||||
>
|
||||
{quotedMessageProps && (
|
||||
|
@ -539,7 +557,7 @@ export const CompositionArea = ({
|
|||
</div>
|
||||
)}
|
||||
{draftAttachments.length ? (
|
||||
<div className="module-composition-area__attachment-list">
|
||||
<div className="CompositionArea__attachment-list">
|
||||
<AttachmentList
|
||||
attachments={draftAttachments}
|
||||
i18n={i18n}
|
||||
|
@ -553,12 +571,12 @@ export const CompositionArea = ({
|
|||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-area__row',
|
||||
large ? 'module-composition-area__row--padded' : null
|
||||
'CompositionArea__row',
|
||||
large ? 'CompositionArea__row--padded' : null
|
||||
)}
|
||||
>
|
||||
{!large ? leftHandSideButtonsFragment : null}
|
||||
<div className="module-composition-area__input">
|
||||
<div className="CompositionArea__input">
|
||||
<CompositionInput
|
||||
i18n={i18n}
|
||||
disabled={disabled}
|
||||
|
@ -588,8 +606,8 @@ export const CompositionArea = ({
|
|||
{large ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-area__row',
|
||||
'module-composition-area__row--control-row'
|
||||
'CompositionArea__row',
|
||||
'CompositionArea__row--control-row'
|
||||
)}
|
||||
>
|
||||
{leftHandSideButtonsFragment}
|
||||
|
|
|
@ -118,3 +118,15 @@ story.add('media attachments', () => {
|
|||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('announcement only groups non-admin', () => (
|
||||
<ForwardMessageModal
|
||||
{...createProps()}
|
||||
candidateConversations={[
|
||||
getDefaultConversation({
|
||||
announcementsOnly: true,
|
||||
areWeAdmin: false,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -17,6 +17,7 @@ import { AttachmentList } from './conversation/AttachmentList';
|
|||
import { AttachmentType } from '../types/Attachment';
|
||||
import { Button } from './Button';
|
||||
import { CompositionInput, InputApi } from './CompositionInput';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
||||
import { ConversationList, Row, RowType } from './ConversationList';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
@ -92,6 +93,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
const [attachmentsToForward, setAttachmentsToForward] = useState(attachments);
|
||||
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
||||
const [messageBodyText, setMessageBodyText] = useState(messageBody || '');
|
||||
const [cannotMessage, setCannotMessage] = useState(false);
|
||||
|
||||
const isMessageEditable = !isSticker;
|
||||
|
||||
|
@ -186,7 +188,11 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
}
|
||||
const selectedContact = contactLookup.get(conversationId);
|
||||
if (selectedContact) {
|
||||
setSelectedContacts([...nextSelectedContacts, selectedContact]);
|
||||
if (selectedContact.announcementsOnly && !selectedContact.areWeAdmin) {
|
||||
setCannotMessage(true);
|
||||
} else {
|
||||
setSelectedContacts([...nextSelectedContacts, selectedContact]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[contactLookup, selectedContacts, setSelectedContacts]
|
||||
|
@ -233,183 +239,194 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<ModalHost onEscape={handleBackOrClose} onClose={onClose}>
|
||||
<div className="module-ForwardMessageModal">
|
||||
<div
|
||||
className={classNames('module-ForwardMessageModal__header', {
|
||||
'module-ForwardMessageModal__header--edit': isEditingMessage,
|
||||
})}
|
||||
<>
|
||||
{cannotMessage && (
|
||||
<ConfirmationDialog
|
||||
cancelText={i18n('Confirmation--confirm')}
|
||||
i18n={i18n}
|
||||
onClose={() => setCannotMessage(false)}
|
||||
>
|
||||
{i18n('GroupV2--cannot-send')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
<ModalHost onEscape={handleBackOrClose} onClose={onClose}>
|
||||
<div className="module-ForwardMessageModal">
|
||||
<div
|
||||
className={classNames('module-ForwardMessageModal__header', {
|
||||
'module-ForwardMessageModal__header--edit': isEditingMessage,
|
||||
})}
|
||||
>
|
||||
{isEditingMessage ? (
|
||||
<button
|
||||
aria-label={i18n('back')}
|
||||
className="module-ForwardMessageModal__header--back"
|
||||
onClick={() => setIsEditingMessage(false)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="module-ForwardMessageModal__header--close"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
<h1>{i18n('forwardMessage')}</h1>
|
||||
</div>
|
||||
{isEditingMessage ? (
|
||||
<button
|
||||
aria-label={i18n('back')}
|
||||
className="module-ForwardMessageModal__header--back"
|
||||
onClick={() => setIsEditingMessage(false)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="module-ForwardMessageModal__header--close"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
<h1>{i18n('forwardMessage')}</h1>
|
||||
</div>
|
||||
{isEditingMessage ? (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
{linkPreview ? (
|
||||
<div className="module-ForwardMessageModal--link-preview">
|
||||
<StagedLinkPreview
|
||||
date={linkPreview.date || null}
|
||||
description={linkPreview.description || ''}
|
||||
domain={linkPreview.url}
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
{linkPreview ? (
|
||||
<div className="module-ForwardMessageModal--link-preview">
|
||||
<StagedLinkPreview
|
||||
date={linkPreview.date || null}
|
||||
description={linkPreview.description || ''}
|
||||
domain={linkPreview.url}
|
||||
i18n={i18n}
|
||||
image={linkPreview.image}
|
||||
onClose={() => removeLinkPreview()}
|
||||
title={linkPreview.title}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{attachmentsToForward && attachmentsToForward.length ? (
|
||||
<AttachmentList
|
||||
attachments={attachmentsToForward}
|
||||
i18n={i18n}
|
||||
image={linkPreview.image}
|
||||
onClose={() => removeLinkPreview()}
|
||||
title={linkPreview.title}
|
||||
onCloseAttachment={(attachment: AttachmentType) => {
|
||||
const newAttachments = attachmentsToForward.filter(
|
||||
currentAttachment => currentAttachment !== attachment
|
||||
);
|
||||
setAttachmentsToForward(newAttachments);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{attachmentsToForward && attachmentsToForward.length ? (
|
||||
<AttachmentList
|
||||
attachments={attachmentsToForward}
|
||||
i18n={i18n}
|
||||
onCloseAttachment={(attachment: AttachmentType) => {
|
||||
const newAttachments = attachmentsToForward.filter(
|
||||
currentAttachment => currentAttachment !== attachment
|
||||
);
|
||||
setAttachmentsToForward(newAttachments);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-ForwardMessageModal__text-edit-area">
|
||||
<CompositionInput
|
||||
clearQuotedMessage={shouldNeverBeCalled}
|
||||
draftText={messageBodyText}
|
||||
getQuotedMessage={noop}
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
large
|
||||
moduleClassName="module-ForwardMessageModal__input"
|
||||
onEditorStateChange={(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
caretLocation
|
||||
) => {
|
||||
setMessageBodyText(messageText);
|
||||
onEditorStateChange(messageText, bodyRanges, caretLocation);
|
||||
}}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={forwardMessage}
|
||||
onTextTooLong={onTextTooLong}
|
||||
/>
|
||||
<div className="module-ForwardMessageModal__emoji">
|
||||
<EmojiButton
|
||||
) : null}
|
||||
<div className="module-ForwardMessageModal__text-edit-area">
|
||||
<CompositionInput
|
||||
clearQuotedMessage={shouldNeverBeCalled}
|
||||
draftText={messageBodyText}
|
||||
getQuotedMessage={noop}
|
||||
i18n={i18n}
|
||||
onClose={focusTextEditInput}
|
||||
onPickEmoji={insertEmoji}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
inputApi={inputApiRef}
|
||||
large
|
||||
moduleClassName="module-ForwardMessageModal__input"
|
||||
onEditorStateChange={(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
caretLocation
|
||||
) => {
|
||||
setMessageBodyText(messageText);
|
||||
onEditorStateChange(messageText, bodyRanges, caretLocation);
|
||||
}}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={forwardMessage}
|
||||
onTextTooLong={onTextTooLong}
|
||||
/>
|
||||
<div className="module-ForwardMessageModal__emoji">
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
onClose={focusTextEditInput}
|
||||
onPickEmoji={insertEmoji}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
onChange={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
||||
// We disable this ESLint rule because we're capturing a bubbled keydown
|
||||
// event. See [this note in the jsx-a11y docs][0].
|
||||
//
|
||||
// [0]: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/c275964f52c35775208bd00cb612c6f82e42e34f/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
return (
|
||||
<div
|
||||
className="module-ForwardMessageModal__list-wrapper"
|
||||
ref={measureRef}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={contentRect.bounds}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
disabledReason:
|
||||
| undefined
|
||||
| ContactCheckboxDisabledReason
|
||||
) => {
|
||||
if (
|
||||
disabledReason !==
|
||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||
) {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}
|
||||
}}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
return <div />;
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
onChange={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
</Measure>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
||||
{i18n('noContactsFound')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="module-ForwardMessageModal__footer">
|
||||
<div>
|
||||
{Boolean(selectedContacts.length) &&
|
||||
selectedContacts.map(contact => contact.title).join(', ')}
|
||||
</div>
|
||||
<div>
|
||||
{isEditingMessage || !isMessageEditable ? (
|
||||
<Button
|
||||
aria-label={i18n('ForwardMessageModal--continue')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
|
||||
disabled={!canForwardMessage}
|
||||
onClick={forwardMessage}
|
||||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={i18n('forwardMessage')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
|
||||
disabled={!hasContactsSelected}
|
||||
onClick={() => setIsEditingMessage(true)}
|
||||
/>
|
||||
)}
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
||||
// We disable this ESLint rule because we're capturing a bubbled
|
||||
// keydown event. See [this note in the jsx-a11y docs][0].
|
||||
//
|
||||
// [0]: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/c275964f52c35775208bd00cb612c6f82e42e34f/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
return (
|
||||
<div
|
||||
className="module-ForwardMessageModal__list-wrapper"
|
||||
ref={measureRef}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={contentRect.bounds}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
disabledReason:
|
||||
| undefined
|
||||
| ContactCheckboxDisabledReason
|
||||
) => {
|
||||
if (
|
||||
disabledReason !==
|
||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||
) {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}
|
||||
}}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
return <div />;
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
}}
|
||||
</Measure>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
||||
{i18n('noContactsFound')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="module-ForwardMessageModal__footer">
|
||||
<div>
|
||||
{Boolean(selectedContacts.length) &&
|
||||
selectedContacts.map(contact => contact.title).join(', ')}
|
||||
</div>
|
||||
<div>
|
||||
{isEditingMessage || !isMessageEditable ? (
|
||||
<Button
|
||||
aria-label={i18n('ForwardMessageModal--continue')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
|
||||
disabled={!canForwardMessage}
|
||||
onClick={forwardMessage}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={i18n('forwardMessage')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
|
||||
disabled={!hasContactsSelected}
|
||||
onClick={() => setIsEditingMessage(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalHost>
|
||||
</ModalHost>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,8 @@ export type PropsDataType = {
|
|||
} & Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'announcementsOnly'
|
||||
| 'areWeAdmin'
|
||||
| 'avatarPath'
|
||||
| 'canChangeTimer'
|
||||
| 'color'
|
||||
|
@ -291,6 +293,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
private renderOutgoingCallButtons(): ReactNode {
|
||||
const {
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
i18n,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
|
@ -301,15 +305,18 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
const videoButton = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOutgoingVideoCallInConversation}
|
||||
aria-label={i18n('makeOutgoingVideoCall')}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--video',
|
||||
showBackButton ? null : 'module-ConversationHeader__button--show'
|
||||
showBackButton ? null : 'module-ConversationHeader__button--show',
|
||||
!showBackButton && announcementsOnly && !areWeAdmin
|
||||
? 'module-ConversationHeader__button--show-disabled'
|
||||
: undefined
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
aria-label={i18n('makeOutgoingVideoCall')}
|
||||
onClick={onOutgoingVideoCallInConversation}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -341,14 +348,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
return (
|
||||
<button
|
||||
aria-label={i18n('joinOngoingCall')}
|
||||
type="button"
|
||||
onClick={onOutgoingVideoCallInConversation}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--join-call',
|
||||
showBackButton ? null : 'module-ConversationHeader__button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
onClick={onOutgoingVideoCallInConversation}
|
||||
type="button"
|
||||
>
|
||||
{isNarrow ? null : i18n('joinOngoingCall')}
|
||||
</button>
|
||||
|
|
|
@ -1383,4 +1383,62 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
|||
)}
|
||||
</>
|
||||
);
|
||||
})
|
||||
.add('Announcement Group (Change)', () => {
|
||||
return (
|
||||
<>
|
||||
{renderChange({
|
||||
from: OUR_ID,
|
||||
details: [
|
||||
{
|
||||
type: 'announcements-only',
|
||||
announcementsOnly: true,
|
||||
},
|
||||
],
|
||||
})}
|
||||
{renderChange({
|
||||
from: ADMIN_A,
|
||||
details: [
|
||||
{
|
||||
type: 'announcements-only',
|
||||
announcementsOnly: true,
|
||||
},
|
||||
],
|
||||
})}
|
||||
{renderChange({
|
||||
details: [
|
||||
{
|
||||
type: 'announcements-only',
|
||||
announcementsOnly: true,
|
||||
},
|
||||
],
|
||||
})}
|
||||
{renderChange({
|
||||
from: OUR_ID,
|
||||
details: [
|
||||
{
|
||||
type: 'announcements-only',
|
||||
announcementsOnly: false,
|
||||
},
|
||||
],
|
||||
})}
|
||||
{renderChange({
|
||||
from: ADMIN_A,
|
||||
details: [
|
||||
{
|
||||
type: 'announcements-only',
|
||||
announcementsOnly: false,
|
||||
},
|
||||
],
|
||||
})}
|
||||
{renderChange({
|
||||
details: [
|
||||
{
|
||||
type: 'announcements-only',
|
||||
announcementsOnly: false,
|
||||
},
|
||||
],
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -139,3 +139,15 @@ story.add('Group Links On', () => {
|
|||
|
||||
return <ConversationDetails {...props} isAdmin />;
|
||||
});
|
||||
|
||||
story.add('Group add with missing capabilities', () => (
|
||||
<ConversationDetails
|
||||
{...createProps()}
|
||||
canEditGroupInfo
|
||||
addMembers={async () => {
|
||||
const error = new Error();
|
||||
error.code = 'E_NO_CAPABILITY';
|
||||
throw error;
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { EditConversationAttributesModal } from './EditConversationAttributesModal';
|
||||
import { RequestState } from './util';
|
||||
import { getCustomColorStyle } from '../../../util/getCustomColorStyle';
|
||||
import { ConfirmationDialog } from '../../ConfirmationDialog';
|
||||
|
||||
enum ModalState {
|
||||
NothingOpen,
|
||||
|
@ -109,6 +110,9 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
addGroupMembersRequestState,
|
||||
setAddGroupMembersRequestState,
|
||||
] = useState<RequestState>(RequestState.Inactive);
|
||||
const [membersMissingCapability, setMembersMissingCapability] = useState(
|
||||
false
|
||||
);
|
||||
|
||||
if (conversation === undefined) {
|
||||
throw new Error('ConversationDetails rendered without a conversation');
|
||||
|
@ -194,7 +198,12 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
setModalState(ModalState.NothingOpen);
|
||||
setAddGroupMembersRequestState(RequestState.Inactive);
|
||||
} catch (err) {
|
||||
setAddGroupMembersRequestState(RequestState.InactiveWithError);
|
||||
if (err.code === 'E_NO_CAPABILITY') {
|
||||
setMembersMissingCapability(true);
|
||||
setAddGroupMembersRequestState(RequestState.InactiveWithError);
|
||||
} else {
|
||||
setAddGroupMembersRequestState(RequestState.InactiveWithError);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
|
@ -211,6 +220,16 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
|
||||
return (
|
||||
<div className="conversation-details-panel">
|
||||
{membersMissingCapability && (
|
||||
<ConfirmationDialog
|
||||
cancelText={i18n('Confirmation--confirm')}
|
||||
i18n={i18n}
|
||||
onClose={() => setMembersMissingCapability(false)}
|
||||
>
|
||||
{i18n('GroupV2--add--missing-capability')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
<ConversationDetailsHeader
|
||||
canEdit={canEditGroupInfo}
|
||||
conversation={conversation}
|
||||
|
|
|
@ -27,6 +27,8 @@ const conversation: ConversationType = getDefaultConversation({
|
|||
title: 'Some Conversation',
|
||||
type: 'group',
|
||||
sharedGroupNames: [],
|
||||
announcementsOnlyReady: true,
|
||||
areWeAdmin: true,
|
||||
});
|
||||
|
||||
const createProps = (): PropsType => ({
|
||||
|
@ -36,6 +38,7 @@ const createProps = (): PropsType => ({
|
|||
'setAccessControlAttributesSetting'
|
||||
),
|
||||
setAccessControlMembersSetting: action('setAccessControlMembersSetting'),
|
||||
setAnnouncementsOnly: action('setAnnouncementsOnly'),
|
||||
});
|
||||
|
||||
story.add('Basic', () => {
|
||||
|
|
|
@ -6,6 +6,7 @@ import React from 'react';
|
|||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
import { getAccessControlOptions } from '../../../util/getAccessControlOptions';
|
||||
import { SignalService as Proto } from '../../../protobuf';
|
||||
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { PanelSection } from './PanelSection';
|
||||
|
@ -16,14 +17,16 @@ export type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
setAccessControlAttributesSetting: (value: number) => void;
|
||||
setAccessControlMembersSetting: (value: number) => void;
|
||||
setAnnouncementsOnly: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const GroupV2Permissions: React.ComponentType<PropsType> = ({
|
||||
export const GroupV2Permissions = ({
|
||||
conversation,
|
||||
i18n,
|
||||
setAccessControlAttributesSetting,
|
||||
setAccessControlMembersSetting,
|
||||
}) => {
|
||||
setAnnouncementsOnly,
|
||||
}: PropsType): JSX.Element => {
|
||||
if (conversation === undefined) {
|
||||
throw new Error('GroupV2Permissions rendered without a conversation');
|
||||
}
|
||||
|
@ -34,7 +37,16 @@ export const GroupV2Permissions: React.ComponentType<PropsType> = ({
|
|||
const updateAccessControlMembers = (value: string) => {
|
||||
setAccessControlMembersSetting(Number(value));
|
||||
};
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
const updateAnnouncementsOnly = (value: string) => {
|
||||
setAnnouncementsOnly(Number(value) === AccessControlEnum.ADMINISTRATOR);
|
||||
};
|
||||
const accessControlOptions = getAccessControlOptions(i18n);
|
||||
const announcementsOnlyValue = String(
|
||||
conversation.announcementsOnly
|
||||
? AccessControlEnum.ADMINISTRATOR
|
||||
: AccessControlEnum.MEMBER
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
|
@ -60,6 +72,19 @@ export const GroupV2Permissions: React.ComponentType<PropsType> = ({
|
|||
/>
|
||||
}
|
||||
/>
|
||||
{conversation.areWeAdmin && conversation.announcementsOnlyReady && (
|
||||
<PanelRow
|
||||
label={i18n('ConversationDetails--announcement-label')}
|
||||
info={i18n('ConversationDetails--announcement-info')}
|
||||
right={
|
||||
<Select
|
||||
onChange={updateAnnouncementsOnly}
|
||||
options={accessControlOptions}
|
||||
value={announcementsOnlyValue}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</PanelSection>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue