Remove group from story feature

This commit is contained in:
Josh Perez 2022-08-30 15:13:32 -04:00 committed by GitHub
parent 0c13ee896a
commit 9d7eaa003f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 584 additions and 268 deletions

View File

@ -7443,6 +7443,14 @@
} }
} }
}, },
"SendStoryModal__delete-story": {
"message": "Delete story",
"description": "Button label to delete a story"
},
"SendStoryModal__confirm-remove-group": {
"message": "Remove story? This will remove the story from your list, but you will still be able to view stories from this group.",
"description": "Confirmation body for removing a group story"
},
"Stories__settings-toggle--title": { "Stories__settings-toggle--title": {
"message": "Share & View Stories", "message": "Share & View Stories",
"description": "Select box title for the stories on/off toggle" "description": "Select box title for the stories on/off toggle"

View File

@ -27,6 +27,16 @@
} }
&__icon { &__icon {
&--delete {
@include color-svg(
'../images/icons/v2/trash-outline-24.svg',
$color-white
);
height: 14px;
margin-top: 2px;
width: 14px;
}
&--lock { &--lock {
@include color-svg( @include color-svg(
'../images/icons/v2/lock-outline-24.svg', '../images/icons/v2/lock-outline-24.svg',
@ -46,6 +56,28 @@
margin-top: 2px; margin-top: 2px;
width: 14px; width: 14px;
} }
&--settings {
@include color-svg(
'../images/icons/v2/settings-outline-16.svg',
$color-white
);
height: 14px;
margin-top: 2px;
width: 14px;
}
}
&__distribution-list-context {
&__container {
width: 100%;
}
&__button {
align-items: center;
display: flex;
width: 100%;
}
} }
&__distribution-list { &__distribution-list {

View File

@ -14,11 +14,16 @@ import {
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import { import {
getMyStories, getMyStories,
getFakeDistributionLists, getFakeDistributionListsWithMembers,
} from '../test-both/helpers/getFakeDistributionLists'; } from '../test-both/helpers/getFakeDistributionLists';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const myStories = {
...getMyStories(),
members: [],
};
export default { export default {
title: 'Components/SendStoryModal', title: 'Components/SendStoryModal',
component: SendStoryModal, component: SendStoryModal,
@ -27,7 +32,7 @@ export default {
defaultValue: Array.from(Array(100), () => getDefaultConversation()), defaultValue: Array.from(Array(100), () => getDefaultConversation()),
}, },
distributionLists: { distributionLists: {
defaultValue: [getMyStories()], defaultValue: [myStories],
}, },
getPreferredBadge: { action: true }, getPreferredBadge: { action: true },
groupConversations: { groupConversations: {
@ -46,6 +51,7 @@ export default {
defaultValue: getDefaultConversation(), defaultValue: getDefaultConversation(),
}, },
onClose: { action: true }, onClose: { action: true },
onDeleteList: { action: true },
onDistributionListCreated: { action: true }, onDistributionListCreated: { action: true },
onHideMyStoriesFrom: { action: true }, onHideMyStoriesFrom: { action: true },
onSend: { action: true }, onSend: { action: true },
@ -54,7 +60,7 @@ export default {
signalConnections: { signalConnections: {
defaultValue: Array.from(Array(42), getDefaultConversation), defaultValue: Array.from(Array(42), getDefaultConversation),
}, },
tagGroupsAsNewGroupStory: { action: true }, toggleGroupsForStorySend: { action: true },
toggleSignalConnectionsModal: { action: true }, toggleSignalConnectionsModal: { action: true },
}, },
} as Meta; } as Meta;
@ -63,12 +69,25 @@ const Template: Story<PropsType> = args => <SendStoryModal {...args} />;
export const Modal = Template.bind({}); export const Modal = Template.bind({});
Modal.args = { Modal.args = {
distributionLists: getFakeDistributionLists(), distributionLists: getFakeDistributionListsWithMembers(),
}; };
export const FirstTime = Template.bind({}); export const FirstTime = Template.bind({});
FirstTime.args = { FirstTime.args = {
distributionLists: [getMyStories()], distributionLists: [myStories],
groupStories: [],
hasFirstStoryPostExperience: true,
};
export const FirstTimeAlreadyConfiguredOnMobile = Template.bind({});
FirstTime.args = {
distributionLists: [
{
...myStories,
isBlockList: false,
members: Array.from(Array(3), getDefaultConversation),
},
],
groupStories: [], groupStories: [],
hasFirstStoryPostExperience: true, hasFirstStoryPostExperience: true,
}; };

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { noop } from 'lodash';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations'; import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
@ -10,13 +11,15 @@ import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { PropsType as StoriesSettingsModalPropsType } from './StoriesSettingsModal'; import type { PropsType as StoriesSettingsModalPropsType } from './StoriesSettingsModal';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; import type { StoryDistributionListWithMembersDataType } from '../types/Stories';
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { Checkbox } from './Checkbox'; import { Checkbox } from './Checkbox';
import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenu } from './ContextMenu'; import { ContextMenu } from './ContextMenu';
import { import {
DistributionListSettings,
EditDistributionList, EditDistributionList,
EditMyStoriesPrivacy, EditMyStoriesPrivacy,
Page as StoriesSettingsPage, Page as StoriesSettingsPage,
@ -29,7 +32,7 @@ import { isNotNil } from '../util/isNotNil';
export type PropsType = { export type PropsType = {
candidateConversations: Array<ConversationType>; candidateConversations: Array<ConversationType>;
distributionLists: Array<StoryDistributionListDataType>; distributionLists: Array<StoryDistributionListWithMembersDataType>;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
groupConversations: Array<ConversationType>; groupConversations: Array<ConversationType>;
groupStories: Array<ConversationType>; groupStories: Array<ConversationType>;
@ -37,6 +40,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
me: ConversationType; me: ConversationType;
onClose: () => unknown; onClose: () => unknown;
onDeleteList: (listId: string) => unknown;
onDistributionListCreated: ( onDistributionListCreated: (
name: string, name: string,
viewerUuids: Array<UUIDStringType> viewerUuids: Array<UUIDStringType>
@ -47,23 +51,20 @@ export type PropsType = {
conversationIds: Array<string> conversationIds: Array<string>
) => unknown; ) => unknown;
signalConnections: Array<ConversationType>; signalConnections: Array<ConversationType>;
tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown; toggleGroupsForStorySend: (cids: Array<string>) => unknown;
} & Pick< } & Pick<
StoriesSettingsModalPropsType, StoriesSettingsModalPropsType,
| 'onHideMyStoriesFrom' | 'onHideMyStoriesFrom'
| 'onRemoveMember'
| 'onRepliesNReactionsChanged'
| 'onViewersUpdated' | 'onViewersUpdated'
| 'setMyStoriesToAllSignalConnections' | 'setMyStoriesToAllSignalConnections'
| 'toggleSignalConnectionsModal' | 'toggleSignalConnectionsModal'
>; >;
enum MyStoriesPrivacy {
AllSignalConnections = 'AllSignalConnections',
Exclude = 'Exclude',
OnlyShareWith = 'OnlyShareWith',
}
enum SendStoryPage { enum SendStoryPage {
ChooseGroups = 'ChooseGroups', ChooseGroups = 'ChooseGroups',
EditingDistributionList = 'EditingDistributionList',
SendStory = 'SendStory', SendStory = 'SendStory',
SetMyStoriesPrivacy = 'SetMyStoriesPrivacy', SetMyStoriesPrivacy = 'SetMyStoriesPrivacy',
} }
@ -76,30 +77,32 @@ const Page = {
type PageType = SendStoryPage | StoriesSettingsPage; type PageType = SendStoryPage | StoriesSettingsPage;
function getListMemberUuids( function getListMemberUuids(
list: StoryDistributionListDataType, list: StoryDistributionListWithMembersDataType,
signalConnections: Array<ConversationType> signalConnections: Array<ConversationType>
): Array<string> { ): Array<string> {
const memberUuids = list.members.map(({ uuid }) => uuid).filter(isNotNil);
if (list.id === MY_STORIES_ID && list.isBlockList) { if (list.id === MY_STORIES_ID && list.isBlockList) {
const excludeUuids = new Set<string>(list.memberUuids); const excludeUuids = new Set<string>(memberUuids);
return signalConnections return signalConnections
.map(conversation => conversation.uuid) .map(conversation => conversation.uuid)
.filter(isNotNil) .filter(isNotNil)
.filter(uuid => !excludeUuids.has(uuid)); .filter(uuid => !excludeUuids.has(uuid));
} }
return list.memberUuids; return memberUuids;
} }
function getListViewers( function getListViewers(
list: StoryDistributionListDataType, list: StoryDistributionListWithMembersDataType,
i18n: LocalizerType, i18n: LocalizerType,
signalConnections: Array<ConversationType> signalConnections: Array<ConversationType>
): string { ): string {
let memberCount = list.memberUuids.length; let memberCount = list.members.length;
if (list.id === MY_STORIES_ID && list.isBlockList) { if (list.id === MY_STORIES_ID && list.isBlockList) {
memberCount = list.isBlockList memberCount = list.isBlockList
? signalConnections.length - list.memberUuids.length ? signalConnections.length - list.members.length
: signalConnections.length; : signalConnections.length;
} }
@ -118,14 +121,17 @@ export const SendStoryModal = ({
i18n, i18n,
me, me,
onClose, onClose,
onDeleteList,
onDistributionListCreated, onDistributionListCreated,
onHideMyStoriesFrom, onHideMyStoriesFrom,
onSend, onRemoveMember,
onRepliesNReactionsChanged,
onSelectedStoryList, onSelectedStoryList,
onSend,
onViewersUpdated, onViewersUpdated,
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
signalConnections, signalConnections,
tagGroupsAsNewGroupStory, toggleGroupsForStorySend,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [page, setPage] = useState<PageType>(Page.SendStory); const [page, setPage] = useState<PageType>(Page.SendStory);
@ -192,27 +198,61 @@ export const SendStoryModal = ({
Array<ConversationType> Array<ConversationType>
>([]); >([]);
const [myStoriesPrivacy, setMyStoriesPrivacy] = useState<MyStoriesPrivacy>( const [confirmRemoveGroupId, setConfirmRemoveGroupId] = useState<
MyStoriesPrivacy.AllSignalConnections string | undefined
); >();
const [myStoriesPrivacyUuids, setMyStoriesPrivacyUuids] = useState< const [confirmDeleteListId, setConfirmDeleteListId] = useState<
Set<UUIDStringType> string | undefined
>(new Set()); >();
const myStories = useMemo(() => { const [listIdToEdit, setListIdToEdit] = useState<string | undefined>();
return {
useEffect(() => {
if (listIdToEdit) {
setPage(Page.EditingDistributionList);
} else {
setPage(Page.SendStory);
}
}, [listIdToEdit]);
const listToEdit = useMemo(() => {
if (!listIdToEdit) {
return;
}
return distributionLists.find(list => list.id === listIdToEdit);
}, [distributionLists, listIdToEdit]);
// myStoriesPrivacy, myStoriesPrivacyUuids, and myStories are only used
// during the first time posting to My Stories experience where we have
// to select the privacy settings.
const ogMyStories = useMemo(
() => distributionLists.find(list => list.id === MY_STORIES_ID),
[distributionLists]
);
const initialMyStories = useMemo(
() => ({
allowsReplies: true, allowsReplies: true,
id: MY_STORIES_ID, id: MY_STORIES_ID,
name: MY_STORIES_ID, name: i18n('Stories__mine'),
isBlockList: myStoriesPrivacy !== MyStoriesPrivacy.OnlyShareWith, isBlockList: ogMyStories?.isBlockList ?? true,
members: members: ogMyStories?.members || [],
myStoriesPrivacy === MyStoriesPrivacy.AllSignalConnections }),
? [] [i18n, ogMyStories]
: candidateConversations.filter( );
convo => convo.uuid && myStoriesPrivacyUuids.has(convo.uuid)
), const initialMyStoriesMemberUuids = useMemo(
}; () => (ogMyStories?.members || []).map(({ uuid }) => uuid).filter(isNotNil),
}, [candidateConversations, myStoriesPrivacy, myStoriesPrivacyUuids]); [ogMyStories]
);
const [stagedMyStories, setStagedMyStories] =
useState<StoryDistributionListWithMembersDataType>(initialMyStories);
const [stagedMyStoriesMemberUuids, setStagedMyStoriesMemberUuids] = useState<
Array<UUIDStringType>
>(initialMyStoriesMemberUuids);
let content: JSX.Element; let content: JSX.Element;
if (page === Page.SetMyStoriesPrivacy) { if (page === Page.SetMyStoriesPrivacy) {
@ -221,28 +261,63 @@ export const SendStoryModal = ({
hasDisclaimerAbove hasDisclaimerAbove
i18n={i18n} i18n={i18n}
learnMore="SendStoryModal__privacy-disclaimer" learnMore="SendStoryModal__privacy-disclaimer"
myStories={myStories} myStories={stagedMyStories}
onClickExclude={() => { onClickExclude={() => {
setMyStoriesPrivacy(MyStoriesPrivacy.Exclude); let nextSelectedContacts = stagedMyStories.members;
setMyStoriesPrivacyUuids(new Set());
setSelectedContacts([]); if (!stagedMyStories.isBlockList) {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: true,
members: [],
}));
nextSelectedContacts = [];
}
setSelectedContacts(nextSelectedContacts);
setPage(Page.HideStoryFrom); setPage(Page.HideStoryFrom);
}} }}
onClickOnlyShareWith={() => { onClickOnlyShareWith={() => {
setMyStoriesPrivacy(MyStoriesPrivacy.OnlyShareWith); if (!stagedMyStories.isBlockList) {
setMyStoriesPrivacyUuids(new Set()); setSelectedContacts(stagedMyStories.members);
setSelectedContacts([]); } else {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: false,
members: [],
}));
}
setPage(Page.AddViewer); setPage(Page.AddViewer);
}} }}
setSelectedContacts={setSelectedContacts} setSelectedContacts={setSelectedContacts}
setMyStoriesToAllSignalConnections={() => { setMyStoriesToAllSignalConnections={() => {
setMyStoriesPrivacy(MyStoriesPrivacy.AllSignalConnections); setStagedMyStories(myStories => ({
setMyStoriesPrivacyUuids(new Set()); ...myStories,
isBlockList: true,
members: [],
}));
setSelectedContacts([]); setSelectedContacts([]);
}} }}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/> />
); );
} else if (page === Page.EditingDistributionList && listToEdit) {
content = (
<DistributionListSettings
getPreferredBadge={getPreferredBadge}
i18n={i18n}
listToEdit={listToEdit}
onRemoveMember={onRemoveMember}
onRepliesNReactionsChanged={onRepliesNReactionsChanged}
setConfirmDeleteListId={setConfirmDeleteListId}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
setPage={setPage}
setSelectedContacts={setSelectedContacts}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
);
} else if ( } else if (
page === Page.ChooseViewers || page === Page.ChooseViewers ||
page === Page.NameStory || page === Page.NameStory ||
@ -254,15 +329,21 @@ export const SendStoryModal = ({
candidateConversations={candidateConversations} candidateConversations={candidateConversations}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
onDone={(name, uuids) => { onCreateList={(name, uuids) => {
onDistributionListCreated(name, uuids); onDistributionListCreated(name, uuids);
setPage(Page.SendStory); setPage(Page.SendStory);
}} }}
onViewersUpdated={uuids => { onViewersUpdated={uuids => {
if (page === Page.ChooseViewers) { if (listIdToEdit && page === Page.AddViewer) {
onViewersUpdated(listIdToEdit, uuids);
setPage(Page.EditingDistributionList);
} else if (page === Page.ChooseViewers) {
setPage(Page.NameStory); setPage(Page.NameStory);
} else if (listIdToEdit && page === Page.HideStoryFrom) {
onHideMyStoriesFrom(uuids);
setPage(Page.SendStory);
} else if (page === Page.HideStoryFrom || page === Page.AddViewer) { } else if (page === Page.HideStoryFrom || page === Page.AddViewer) {
setMyStoriesPrivacyUuids(new Set(uuids)); setStagedMyStoriesMemberUuids(uuids);
setPage(Page.SetMyStoriesPrivacy); setPage(Page.SetMyStoriesPrivacy);
} else { } else {
setPage(Page.SendStory); setPage(Page.SendStory);
@ -415,7 +496,38 @@ export const SendStoryModal = ({
}} }}
> >
{({ id, checkboxNode }) => ( {({ id, checkboxNode }) => (
<> <ContextMenu
i18n={i18n}
menuOptions={
list.id === MY_STORIES_ID
? [
{
label: i18n('StoriesSettings__context-menu'),
icon: 'SendStoryModal__icon--delete',
onClick: () => setListIdToEdit(list.id),
},
]
: [
{
label: i18n('StoriesSettings__context-menu'),
icon: 'SendStoryModal__icon--settings',
onClick: () => setListIdToEdit(list.id),
},
{
label: i18n('SendStoryModal__delete-story'),
icon: 'SendStoryModal__icon--delete',
onClick: () => setConfirmDeleteListId(list.id),
},
]
}
moduleClassName="SendStoryModal__distribution-list-context"
onClick={noop}
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
theme={Theme.Dark}
>
<label <label
className="SendStoryModal__distribution-list__label" className="SendStoryModal__distribution-list__label"
htmlFor={id} htmlFor={id}
@ -454,7 +566,7 @@ export const SendStoryModal = ({
</div> </div>
</label> </label>
{checkboxNode} {checkboxNode}
</> </ContextMenu>
)} )}
</Checkbox> </Checkbox>
))} ))}
@ -484,7 +596,23 @@ export const SendStoryModal = ({
}} }}
> >
{({ id, checkboxNode }) => ( {({ id, checkboxNode }) => (
<> <ContextMenu
i18n={i18n}
menuOptions={[
{
label: i18n('SendStoryModal__delete-story'),
icon: 'SendStoryModal__icon--delete',
onClick: () => setConfirmRemoveGroupId(group.id),
},
]}
moduleClassName="SendStoryModal__distribution-list-context"
onClick={noop}
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
theme={Theme.Dark}
>
<label <label
className="SendStoryModal__distribution-list__label" className="SendStoryModal__distribution-list__label"
htmlFor={id} htmlFor={id}
@ -517,7 +645,7 @@ export const SendStoryModal = ({
</div> </div>
</label> </label>
{checkboxNode} {checkboxNode}
</> </ContextMenu>
)} )}
</Checkbox> </Checkbox>
))} ))}
@ -568,7 +696,7 @@ export const SendStoryModal = ({
className="SendStoryModal__ok" className="SendStoryModal__ok"
disabled={!chosenGroupIds.size} disabled={!chosenGroupIds.size}
onClick={() => { onClick={() => {
tagGroupsAsNewGroupStory(Array.from(chosenGroupIds)); toggleGroupsForStorySend(Array.from(chosenGroupIds));
setChosenGroupIds(new Set()); setChosenGroupIds(new Set());
setPage(Page.SendStory); setPage(Page.SendStory);
}} }}
@ -598,21 +726,16 @@ export const SendStoryModal = ({
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
if ( if (stagedMyStories.isBlockList) {
myStoriesPrivacy === MyStoriesPrivacy.AllSignalConnections if (stagedMyStories.members.length) {
) { onHideMyStoriesFrom(stagedMyStoriesMemberUuids);
setMyStoriesToAllSignalConnections(); } else {
} else if (myStoriesPrivacy === MyStoriesPrivacy.Exclude) { setMyStoriesToAllSignalConnections();
onHideMyStoriesFrom(Array.from(myStoriesPrivacyUuids)); }
} else if ( } else {
myStoriesPrivacy === MyStoriesPrivacy.OnlyShareWith onViewersUpdated(MY_STORIES_ID, stagedMyStoriesMemberUuids);
) {
onViewersUpdated(
MY_STORIES_ID,
Array.from(myStoriesPrivacyUuids)
);
} }
setMyStoriesPrivacyUuids(new Set());
setSelectedContacts([]); setSelectedContacts([]);
setPage(Page.SendStory); setPage(Page.SendStory);
}} }}
@ -628,43 +751,97 @@ export const SendStoryModal = ({
} }
return ( return (
<Modal <>
hasStickyButtons <Modal
hasXButton hasStickyButtons
i18n={i18n} hasXButton
modalFooter={modalFooter} i18n={i18n}
onBackButtonClick={ modalFooter={modalFooter}
hasBackButton onBackButtonClick={
? () => { hasBackButton
if (page === Page.SetMyStoriesPrivacy) { ? () => {
setSelectedContacts([]); if (listIdToEdit) {
setMyStoriesPrivacyUuids(new Set()); if (
setMyStoriesPrivacy(MyStoriesPrivacy.AllSignalConnections); page === Page.AddViewer ||
setPage(Page.SendStory); page === Page.HideStoryFrom ||
} else if ( page === Page.ChooseViewers
page === Page.HideStoryFrom || ) {
page === Page.AddViewer setPage(Page.EditingDistributionList);
) { } else {
setSelectedContacts([]); setListIdToEdit(undefined);
setMyStoriesPrivacyUuids(new Set()); }
setPage(Page.SetMyStoriesPrivacy); } else if (page === Page.SetMyStoriesPrivacy) {
} else if (page === Page.ChooseGroups) { setSelectedContacts([]);
setChosenGroupIds(new Set()); setStagedMyStories(initialMyStories);
setPage(Page.SendStory); setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids);
} else if (page === Page.ChooseViewers) { setPage(Page.SendStory);
setSelectedContacts([]); } else if (
setPage(Page.SendStory); page === Page.HideStoryFrom ||
} else if (page === Page.NameStory) { page === Page.AddViewer
setPage(Page.ChooseViewers); ) {
setSelectedContacts([]);
setStagedMyStories(initialMyStories);
setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids);
setPage(Page.SetMyStoriesPrivacy);
} else if (page === Page.ChooseGroups) {
setChosenGroupIds(new Set());
setPage(Page.SendStory);
} else if (page === Page.ChooseViewers) {
setSelectedContacts([]);
setPage(Page.SendStory);
} else if (page === Page.NameStory) {
setPage(Page.ChooseViewers);
}
} }
} : undefined
: undefined }
} onClose={onClose}
onClose={onClose} title={modalTitle}
title={modalTitle} theme={Theme.Dark}
theme={Theme.Dark} >
> {content}
{content} </Modal>
</Modal> {confirmRemoveGroupId && (
<ConfirmationDialog
actions={[
{
action: () => {
toggleGroupsForStorySend([confirmRemoveGroupId]);
setConfirmRemoveGroupId(undefined);
},
style: 'negative',
text: i18n('delete'),
},
]}
i18n={i18n}
onClose={() => {
setConfirmRemoveGroupId(undefined);
}}
>
{i18n('SendStoryModal__confirm-remove-group')}
</ConfirmationDialog>
)}
{confirmDeleteListId && (
<ConfirmationDialog
actions={[
{
action: () => {
onDeleteList(confirmDeleteListId);
setConfirmDeleteListId(undefined);
// setListToEditId(undefined);
},
style: 'negative',
text: i18n('delete'),
},
]}
i18n={i18n}
onClose={() => {
setConfirmDeleteListId(undefined);
}}
>
{i18n('StoriesSettings__delete-list--confirm')}
</ConfirmationDialog>
)}
</>
); );
}; };

View File

@ -117,14 +117,6 @@ export const StoriesSettingsModal = ({
const [confirmDeleteListId, setConfirmDeleteListId] = useState< const [confirmDeleteListId, setConfirmDeleteListId] = useState<
string | undefined string | undefined
>(); >();
const [confirmRemoveMember, setConfirmRemoveMember] = useState<
| undefined
| {
listId: string;
title: string;
uuid: UUIDStringType | undefined;
}
>();
let content: JSX.Element | null; let content: JSX.Element | null;
@ -134,7 +126,7 @@ export const StoriesSettingsModal = ({
candidateConversations={candidateConversations} candidateConversations={candidateConversations}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
onDone={(name, uuids) => { onCreateList={(name, uuids) => {
onDistributionListCreated(name, uuids); onDistributionListCreated(name, uuids);
resetChooseViewersScreen(); resetChooseViewersScreen();
}} }}
@ -159,142 +151,19 @@ export const StoriesSettingsModal = ({
/> />
); );
} else if (listToEdit) { } else if (listToEdit) {
const isMyStories = listToEdit.id === MY_STORIES_ID;
content = ( content = (
<> <DistributionListSettings
{!isMyStories && ( getPreferredBadge={getPreferredBadge}
<> i18n={i18n}
<div className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"> listToEdit={listToEdit}
<span className="StoriesSettingsModal__list__left"> onRemoveMember={onRemoveMember}
<span className="StoriesSettingsModal__list__avatar--private" /> onRepliesNReactionsChanged={onRepliesNReactionsChanged}
<span className="StoriesSettingsModal__list__title"> setConfirmDeleteListId={setConfirmDeleteListId}
<StoryDistributionListName setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
i18n={i18n} setPage={setPage}
id={listToEdit.id} setSelectedContacts={setSelectedContacts}
name={listToEdit.name} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/> />
</span>
</span>
</div>
<hr className="StoriesSettingsModal__divider" />
</>
)}
<div className="StoriesSettingsModal__title">
{i18n('StoriesSettings__who-can-see')}
</div>
{isMyStories && (
<EditMyStoriesPrivacy
i18n={i18n}
learnMore="StoriesSettings__mine_disclaimer"
myStories={listToEdit}
onClickExclude={() => {
setPage(Page.HideStoryFrom);
}}
onClickOnlyShareWith={() => {
setPage(Page.AddViewer);
}}
setSelectedContacts={setSelectedContacts}
setMyStoriesToAllSignalConnections={
setMyStoriesToAllSignalConnections
}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
)}
{!isMyStories && (
<>
<button
className="StoriesSettingsModal__list"
onClick={() => {
setSelectedContacts(listToEdit.members);
setPage(Page.AddViewer);
}}
type="button"
>
<span className="StoriesSettingsModal__list__left">
<span className="StoriesSettingsModal__list__avatar--new" />
<span className="StoriesSettingsModal__list__title">
{i18n('StoriesSettings__add-viewer')}
</span>
</span>
</button>
{listToEdit.members.map(member => (
<div
className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"
key={member.id}
>
<span className="StoriesSettingsModal__list__left">
<Avatar
acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath}
badge={getPreferredBadge(member.badges)}
color={member.color}
conversationType={member.type}
i18n={i18n}
isMe
sharedGroupNames={member.sharedGroupNames}
size={AvatarSize.THIRTY_SIX}
theme={ThemeType.dark}
title={member.title}
/>
<span className="StoriesSettingsModal__list__title">
{member.title}
</span>
</span>
<button
aria-label={i18n('StoriesSettings__remove--title', [
member.title,
])}
className="StoriesSettingsModal__list__delete"
onClick={() =>
setConfirmRemoveMember({
listId: listToEdit.id,
title: member.title,
uuid: member.uuid,
})
}
type="button"
/>
</div>
))}
</>
)}
<hr className="StoriesSettingsModal__divider" />
<div className="StoriesSettingsModal__title">
{i18n('StoriesSettings__replies-reactions--title')}
</div>
<Checkbox
checked={listToEdit.allowsReplies}
description={i18n('StoriesSettings__replies-reactions--description')}
label={i18n('StoriesSettings__replies-reactions--label')}
moduleClassName="StoriesSettingsModal__checkbox"
name="replies-reactions"
onChange={value => onRepliesNReactionsChanged(listToEdit.id, value)}
/>
{!isMyStories && (
<>
<hr className="StoriesSettingsModal__divider" />
<button
className="StoriesSettingsModal__delete-list"
onClick={() => setConfirmDeleteListId(listToEdit.id)}
type="button"
>
{i18n('StoriesSettings__delete-list')}
</button>
</>
)}
</>
); );
} else { } else {
const privateStories = distributionLists.filter( const privateStories = distributionLists.filter(
@ -447,6 +316,182 @@ export const StoriesSettingsModal = ({
{i18n('StoriesSettings__delete-list--confirm')} {i18n('StoriesSettings__delete-list--confirm')}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
</>
);
};
type DistributionListSettingsPropsType = {
i18n: LocalizerType;
listToEdit: StoryDistributionListWithMembersDataType;
setConfirmDeleteListId: (id: string) => unknown;
setPage: (page: Page) => unknown;
setSelectedContacts: (contacts: Array<ConversationType>) => unknown;
} & Pick<
PropsType,
| 'getPreferredBadge'
| 'onRemoveMember'
| 'onRepliesNReactionsChanged'
| 'setMyStoriesToAllSignalConnections'
| 'toggleSignalConnectionsModal'
>;
export const DistributionListSettings = ({
getPreferredBadge,
i18n,
listToEdit,
onRemoveMember,
onRepliesNReactionsChanged,
setConfirmDeleteListId,
setMyStoriesToAllSignalConnections,
setPage,
setSelectedContacts,
toggleSignalConnectionsModal,
}: DistributionListSettingsPropsType): JSX.Element => {
const [confirmRemoveMember, setConfirmRemoveMember] = useState<
| undefined
| {
listId: string;
title: string;
uuid: UUIDStringType | undefined;
}
>();
const isMyStories = listToEdit.id === MY_STORIES_ID;
return (
<>
{!isMyStories && (
<>
<div className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer">
<span className="StoriesSettingsModal__list__left">
<span className="StoriesSettingsModal__list__avatar--private" />
<span className="StoriesSettingsModal__list__title">
<StoryDistributionListName
i18n={i18n}
id={listToEdit.id}
name={listToEdit.name}
/>
</span>
</span>
</div>
<hr className="StoriesSettingsModal__divider" />
</>
)}
<div className="StoriesSettingsModal__title">
{i18n('StoriesSettings__who-can-see')}
</div>
{isMyStories && (
<EditMyStoriesPrivacy
i18n={i18n}
learnMore="StoriesSettings__mine_disclaimer"
myStories={listToEdit}
onClickExclude={() => {
setPage(Page.HideStoryFrom);
}}
onClickOnlyShareWith={() => {
setPage(Page.AddViewer);
}}
setSelectedContacts={setSelectedContacts}
setMyStoriesToAllSignalConnections={
setMyStoriesToAllSignalConnections
}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
)}
{!isMyStories && (
<>
<button
className="StoriesSettingsModal__list"
onClick={() => {
setSelectedContacts(listToEdit.members);
setPage(Page.AddViewer);
}}
type="button"
>
<span className="StoriesSettingsModal__list__left">
<span className="StoriesSettingsModal__list__avatar--new" />
<span className="StoriesSettingsModal__list__title">
{i18n('StoriesSettings__add-viewer')}
</span>
</span>
</button>
{listToEdit.members.map(member => (
<div
className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"
key={member.id}
>
<span className="StoriesSettingsModal__list__left">
<Avatar
acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath}
badge={getPreferredBadge(member.badges)}
color={member.color}
conversationType={member.type}
i18n={i18n}
isMe
sharedGroupNames={member.sharedGroupNames}
size={AvatarSize.THIRTY_SIX}
theme={ThemeType.dark}
title={member.title}
/>
<span className="StoriesSettingsModal__list__title">
{member.title}
</span>
</span>
<button
aria-label={i18n('StoriesSettings__remove--title', [
member.title,
])}
className="StoriesSettingsModal__list__delete"
onClick={() =>
setConfirmRemoveMember({
listId: listToEdit.id,
title: member.title,
uuid: member.uuid,
})
}
type="button"
/>
</div>
))}
</>
)}
<hr className="StoriesSettingsModal__divider" />
<div className="StoriesSettingsModal__title">
{i18n('StoriesSettings__replies-reactions--title')}
</div>
<Checkbox
checked={listToEdit.allowsReplies}
description={i18n('StoriesSettings__replies-reactions--description')}
label={i18n('StoriesSettings__replies-reactions--label')}
moduleClassName="StoriesSettingsModal__checkbox"
name="replies-reactions"
onChange={value => onRepliesNReactionsChanged(listToEdit.id, value)}
/>
{!isMyStories && (
<>
<hr className="StoriesSettingsModal__divider" />
<button
className="StoriesSettingsModal__delete-list"
onClick={() => setConfirmDeleteListId(listToEdit.id)}
type="button"
>
{i18n('StoriesSettings__delete-list')}
</button>
</>
)}
{confirmRemoveMember && ( {confirmRemoveMember && (
<ConfirmationDialog <ConfirmationDialog
actions={[ actions={[
@ -581,7 +626,7 @@ export const EditMyStoriesPrivacy = ({
}; };
type EditDistributionListPropsType = { type EditDistributionListPropsType = {
onDone: (name: string, viewerUuids: Array<UUIDStringType>) => unknown; onCreateList: (name: string, viewerUuids: Array<UUIDStringType>) => unknown;
onViewersUpdated: (viewerUuids: Array<UUIDStringType>) => unknown; onViewersUpdated: (viewerUuids: Array<UUIDStringType>) => unknown;
page: Page; page: Page;
selectedContacts: Array<ConversationType>; selectedContacts: Array<ConversationType>;
@ -592,7 +637,7 @@ export const EditDistributionList = ({
candidateConversations, candidateConversations,
getPreferredBadge, getPreferredBadge,
i18n, i18n,
onDone, onCreateList,
onViewersUpdated, onViewersUpdated,
page, page,
selectedContacts, selectedContacts,
@ -716,7 +761,7 @@ export const EditDistributionList = ({
<Button <Button
disabled={!storyName} disabled={!storyName}
onClick={() => { onClick={() => {
onDone(storyName, Array.from(selectedConversationUuids)); onCreateList(storyName, Array.from(selectedConversationUuids));
setStoryName(''); setStoryName('');
}} }}
variant={ButtonVariant.Primary} variant={ButtonVariant.Primary}

View File

@ -12,7 +12,7 @@ import {
getDefaultConversation, getDefaultConversation,
getDefaultGroup, getDefaultGroup,
} from '../test-both/helpers/getDefaultConversation'; } from '../test-both/helpers/getDefaultConversation';
import { getFakeDistributionLists } from '../test-both/helpers/getFakeDistributionLists'; import { getFakeDistributionListsWithMembers } from '../test-both/helpers/getFakeDistributionLists';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -25,7 +25,7 @@ export default {
defaultValue: Array.from(Array(100), getDefaultConversation), defaultValue: Array.from(Array(100), getDefaultConversation),
}, },
debouncedMaybeGrabLinkPreview: { action: true }, debouncedMaybeGrabLinkPreview: { action: true },
distributionLists: { defaultValue: getFakeDistributionLists() }, distributionLists: { defaultValue: getFakeDistributionListsWithMembers() },
getPreferredBadge: { action: true }, getPreferredBadge: { action: true },
groupConversations: { groupConversations: {
defaultValue: Array.from(Array(7), getDefaultGroup), defaultValue: Array.from(Array(7), getDefaultGroup),
@ -47,6 +47,7 @@ export default {
defaultValue: getDefaultConversation(), defaultValue: getDefaultConversation(),
}, },
onClose: { action: true }, onClose: { action: true },
onDeleteList: { action: true },
onDistributionListCreated: { action: true }, onDistributionListCreated: { action: true },
onHideMyStoriesFrom: { action: true }, onHideMyStoriesFrom: { action: true },
onSend: { action: true }, onSend: { action: true },

View File

@ -31,7 +31,6 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
linkPreview?: LinkPreviewType; linkPreview?: LinkPreviewType;
onClose: () => unknown; onClose: () => unknown;
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
onSend: ( onSend: (
listIds: Array<UUIDStringType>, listIds: Array<UUIDStringType>,
conversationIds: Array<string>, conversationIds: Array<string>,
@ -51,12 +50,16 @@ export type PropsType = {
| 'groupStories' | 'groupStories'
| 'hasFirstStoryPostExperience' | 'hasFirstStoryPostExperience'
| 'me' | 'me'
| 'onDeleteList'
| 'onDistributionListCreated' | 'onDistributionListCreated'
| 'onHideMyStoriesFrom' | 'onHideMyStoriesFrom'
| 'onRemoveMember'
| 'onRepliesNReactionsChanged'
| 'onSelectedStoryList'
| 'onViewersUpdated' | 'onViewersUpdated'
| 'setMyStoriesToAllSignalConnections' | 'setMyStoriesToAllSignalConnections'
| 'signalConnections' | 'signalConnections'
| 'tagGroupsAsNewGroupStory' | 'toggleGroupsForStorySend'
| 'toggleSignalConnectionsModal' | 'toggleSignalConnectionsModal'
>; >;
@ -74,8 +77,11 @@ export const StoryCreator = ({
linkPreview, linkPreview,
me, me,
onClose, onClose,
onDeleteList,
onDistributionListCreated, onDistributionListCreated,
onHideMyStoriesFrom, onHideMyStoriesFrom,
onRemoveMember,
onRepliesNReactionsChanged,
onSelectedStoryList, onSelectedStoryList,
onSend, onSend,
onViewersUpdated, onViewersUpdated,
@ -84,7 +90,7 @@ export const StoryCreator = ({
sendStoryModalOpenStateChanged, sendStoryModalOpenStateChanged,
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
signalConnections, signalConnections,
tagGroupsAsNewGroupStory, toggleGroupsForStorySend,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [draftAttachment, setDraftAttachment] = useState< const [draftAttachment, setDraftAttachment] = useState<
@ -141,8 +147,11 @@ export const StoryCreator = ({
i18n={i18n} i18n={i18n}
me={me} me={me}
onClose={() => setDraftAttachment(undefined)} onClose={() => setDraftAttachment(undefined)}
onDeleteList={onDeleteList}
onDistributionListCreated={onDistributionListCreated} onDistributionListCreated={onDistributionListCreated}
onHideMyStoriesFrom={onHideMyStoriesFrom} onHideMyStoriesFrom={onHideMyStoriesFrom}
onRemoveMember={onRemoveMember}
onRepliesNReactionsChanged={onRepliesNReactionsChanged}
onSelectedStoryList={onSelectedStoryList} onSelectedStoryList={onSelectedStoryList}
onSend={(listIds, groupIds) => { onSend={(listIds, groupIds) => {
onSend(listIds, groupIds, draftAttachment); onSend(listIds, groupIds, draftAttachment);
@ -154,7 +163,7 @@ export const StoryCreator = ({
setMyStoriesToAllSignalConnections setMyStoriesToAllSignalConnections
} }
signalConnections={signalConnections} signalConnections={signalConnections}
tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory} toggleGroupsForStorySend={toggleGroupsForStorySend}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/> />
)} )}

View File

@ -861,10 +861,10 @@ export const actions = {
showConversation, showConversation,
startComposing, startComposing,
startSettingGroupMetadata, startSettingGroupMetadata,
tagGroupsAsNewGroupStory,
toggleAdmin, toggleAdmin,
toggleConversationInChooseMembers, toggleConversationInChooseMembers,
toggleComposeEditingAvatar, toggleComposeEditingAvatar,
toggleGroupsForStorySend,
toggleHideStories, toggleHideStories,
updateConversationModelSharedGroups, updateConversationModelSharedGroups,
verifyConversationsStoppingSend, verifyConversationsStoppingSend,
@ -1963,7 +1963,7 @@ function removeMemberFromGroup(
}; };
} }
function tagGroupsAsNewGroupStory( function toggleGroupsForStorySend(
conversationIds: Array<string> conversationIds: Array<string>
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
@ -1974,7 +1974,9 @@ function tagGroupsAsNewGroupStory(
return; return;
} }
conversation.set({ isGroupStorySendReady: true }); conversation.set({
isGroupStorySendReady: !conversation.get('isGroupStorySendReady'),
});
await window.Signal.Data.updateConversation(conversation.attributes); await window.Signal.Data.updateConversation(conversation.attributes);
}) })
); );

View File

@ -15,7 +15,7 @@ import {
getMe, getMe,
getNonGroupStories, getNonGroupStories,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { getDistributionLists } from '../selectors/storyDistributionLists'; import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { import {
getInstalledStickerPacks, getInstalledStickerPacks,
@ -46,17 +46,20 @@ export function SmartStoryCreator({
sendStoryMessage, sendStoryMessage,
verifyStoryListMembers, verifyStoryListMembers,
} = useStoriesActions(); } = useStoriesActions();
const { tagGroupsAsNewGroupStory } = useConversationsActions(); const { toggleGroupsForStorySend } = useConversationsActions();
const { const {
allowsRepliesChanged,
createDistributionList, createDistributionList,
deleteDistributionList,
hideMyStoriesFrom, hideMyStoriesFrom,
removeMemberFromDistributionList,
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
updateStoryViewers, updateStoryViewers,
} = useStoryDistributionListsActions(); } = useStoryDistributionListsActions();
const { toggleSignalConnectionsModal } = useGlobalModalActions(); const { toggleSignalConnectionsModal } = useGlobalModalActions();
const candidateConversations = useSelector(getCandidateContactsForNewGroup); const candidateConversations = useSelector(getCandidateContactsForNewGroup);
const distributionLists = useSelector(getDistributionLists); const distributionLists = useSelector(getDistributionListsWithMembers);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const groupConversations = useSelector(getNonGroupStories); const groupConversations = useSelector(getNonGroupStories);
const groupStories = useSelector(getGroupStories); const groupStories = useSelector(getGroupStories);
@ -83,8 +86,11 @@ export function SmartStoryCreator({
linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)} linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
me={me} me={me}
onClose={onClose} onClose={onClose}
onDeleteList={deleteDistributionList}
onDistributionListCreated={createDistributionList} onDistributionListCreated={createDistributionList}
onHideMyStoriesFrom={hideMyStoriesFrom} onHideMyStoriesFrom={hideMyStoriesFrom}
onRemoveMember={removeMemberFromDistributionList}
onRepliesNReactionsChanged={allowsRepliesChanged}
onSelectedStoryList={verifyStoryListMembers} onSelectedStoryList={verifyStoryListMembers}
onSend={sendStoryMessage} onSend={sendStoryMessage}
onViewersUpdated={updateStoryViewers} onViewersUpdated={updateStoryViewers}
@ -93,7 +99,7 @@ export function SmartStoryCreator({
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged} sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections} setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
signalConnections={signalConnections} signalConnections={signalConnections}
tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory} toggleGroupsForStorySend={toggleGroupsForStorySend}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/> />
); );

View File

@ -4,8 +4,25 @@
import casual from 'casual'; import casual from 'casual';
import type { StoryDistributionListDataType } from '../../state/ducks/storyDistributionLists'; import type { StoryDistributionListDataType } from '../../state/ducks/storyDistributionLists';
import type { StoryDistributionListWithMembersDataType } from '../../types/Stories';
import { MY_STORIES_ID } from '../../types/Stories'; import { MY_STORIES_ID } from '../../types/Stories';
import { UUID } from '../../types/UUID'; import { UUID } from '../../types/UUID';
import { getDefaultConversation } from './getDefaultConversation';
export function getFakeDistributionListsWithMembers(): Array<StoryDistributionListWithMembersDataType> {
return [
{
...getMyStories(),
members: [],
},
...Array.from(Array(casual.integer(2, 8)), () => ({
...getFakeDistributionList(),
members: Array.from(Array(casual.integer(3, 12)), () =>
getDefaultConversation()
),
})),
];
}
export function getFakeDistributionLists(): Array<StoryDistributionListDataType> { export function getFakeDistributionLists(): Array<StoryDistributionListDataType> {
return [ return [