diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ccd524ec5..8051f18bc 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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": { "message": "Share & View Stories", "description": "Select box title for the stories on/off toggle" diff --git a/stylesheets/components/SendStoryModal.scss b/stylesheets/components/SendStoryModal.scss index f0e4fc0fe..7e6f2ac17 100644 --- a/stylesheets/components/SendStoryModal.scss +++ b/stylesheets/components/SendStoryModal.scss @@ -27,6 +27,16 @@ } &__icon { + &--delete { + @include color-svg( + '../images/icons/v2/trash-outline-24.svg', + $color-white + ); + height: 14px; + margin-top: 2px; + width: 14px; + } + &--lock { @include color-svg( '../images/icons/v2/lock-outline-24.svg', @@ -46,6 +56,28 @@ margin-top: 2px; 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 { diff --git a/ts/components/SendStoryModal.stories.tsx b/ts/components/SendStoryModal.stories.tsx index e46643d38..89892a178 100644 --- a/ts/components/SendStoryModal.stories.tsx +++ b/ts/components/SendStoryModal.stories.tsx @@ -14,11 +14,16 @@ import { import { setupI18n } from '../util/setupI18n'; import { getMyStories, - getFakeDistributionLists, + getFakeDistributionListsWithMembers, } from '../test-both/helpers/getFakeDistributionLists'; const i18n = setupI18n('en', enMessages); +const myStories = { + ...getMyStories(), + members: [], +}; + export default { title: 'Components/SendStoryModal', component: SendStoryModal, @@ -27,7 +32,7 @@ export default { defaultValue: Array.from(Array(100), () => getDefaultConversation()), }, distributionLists: { - defaultValue: [getMyStories()], + defaultValue: [myStories], }, getPreferredBadge: { action: true }, groupConversations: { @@ -46,6 +51,7 @@ export default { defaultValue: getDefaultConversation(), }, onClose: { action: true }, + onDeleteList: { action: true }, onDistributionListCreated: { action: true }, onHideMyStoriesFrom: { action: true }, onSend: { action: true }, @@ -54,7 +60,7 @@ export default { signalConnections: { defaultValue: Array.from(Array(42), getDefaultConversation), }, - tagGroupsAsNewGroupStory: { action: true }, + toggleGroupsForStorySend: { action: true }, toggleSignalConnectionsModal: { action: true }, }, } as Meta; @@ -63,12 +69,25 @@ const Template: Story = args => ; export const Modal = Template.bind({}); Modal.args = { - distributionLists: getFakeDistributionLists(), + distributionLists: getFakeDistributionListsWithMembers(), }; export const FirstTime = Template.bind({}); 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: [], hasFirstStoryPostExperience: true, }; diff --git a/ts/components/SendStoryModal.tsx b/ts/components/SendStoryModal.tsx index 5a66030f4..5ec81ec07 100644 --- a/ts/components/SendStoryModal.tsx +++ b/ts/components/SendStoryModal.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { useEffect, useMemo, useState } from 'react'; +import { noop } from 'lodash'; import { SearchInput } from './SearchInput'; import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations'; @@ -10,13 +11,15 @@ import type { ConversationType } from '../state/ducks/conversations'; import type { LocalizerType } from '../types/Util'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; 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 { Avatar, AvatarSize } from './Avatar'; import { Button, ButtonVariant } from './Button'; import { Checkbox } from './Checkbox'; +import { ConfirmationDialog } from './ConfirmationDialog'; import { ContextMenu } from './ContextMenu'; import { + DistributionListSettings, EditDistributionList, EditMyStoriesPrivacy, Page as StoriesSettingsPage, @@ -29,7 +32,7 @@ import { isNotNil } from '../util/isNotNil'; export type PropsType = { candidateConversations: Array; - distributionLists: Array; + distributionLists: Array; getPreferredBadge: PreferredBadgeSelectorType; groupConversations: Array; groupStories: Array; @@ -37,6 +40,7 @@ export type PropsType = { i18n: LocalizerType; me: ConversationType; onClose: () => unknown; + onDeleteList: (listId: string) => unknown; onDistributionListCreated: ( name: string, viewerUuids: Array @@ -47,23 +51,20 @@ export type PropsType = { conversationIds: Array ) => unknown; signalConnections: Array; - tagGroupsAsNewGroupStory: (cids: Array) => unknown; + toggleGroupsForStorySend: (cids: Array) => unknown; } & Pick< StoriesSettingsModalPropsType, | 'onHideMyStoriesFrom' + | 'onRemoveMember' + | 'onRepliesNReactionsChanged' | 'onViewersUpdated' | 'setMyStoriesToAllSignalConnections' | 'toggleSignalConnectionsModal' >; -enum MyStoriesPrivacy { - AllSignalConnections = 'AllSignalConnections', - Exclude = 'Exclude', - OnlyShareWith = 'OnlyShareWith', -} - enum SendStoryPage { ChooseGroups = 'ChooseGroups', + EditingDistributionList = 'EditingDistributionList', SendStory = 'SendStory', SetMyStoriesPrivacy = 'SetMyStoriesPrivacy', } @@ -76,30 +77,32 @@ const Page = { type PageType = SendStoryPage | StoriesSettingsPage; function getListMemberUuids( - list: StoryDistributionListDataType, + list: StoryDistributionListWithMembersDataType, signalConnections: Array ): Array { + const memberUuids = list.members.map(({ uuid }) => uuid).filter(isNotNil); + if (list.id === MY_STORIES_ID && list.isBlockList) { - const excludeUuids = new Set(list.memberUuids); + const excludeUuids = new Set(memberUuids); return signalConnections .map(conversation => conversation.uuid) .filter(isNotNil) .filter(uuid => !excludeUuids.has(uuid)); } - return list.memberUuids; + return memberUuids; } function getListViewers( - list: StoryDistributionListDataType, + list: StoryDistributionListWithMembersDataType, i18n: LocalizerType, signalConnections: Array ): string { - let memberCount = list.memberUuids.length; + let memberCount = list.members.length; if (list.id === MY_STORIES_ID && list.isBlockList) { memberCount = list.isBlockList - ? signalConnections.length - list.memberUuids.length + ? signalConnections.length - list.members.length : signalConnections.length; } @@ -118,14 +121,17 @@ export const SendStoryModal = ({ i18n, me, onClose, + onDeleteList, onDistributionListCreated, onHideMyStoriesFrom, - onSend, + onRemoveMember, + onRepliesNReactionsChanged, onSelectedStoryList, + onSend, onViewersUpdated, setMyStoriesToAllSignalConnections, signalConnections, - tagGroupsAsNewGroupStory, + toggleGroupsForStorySend, toggleSignalConnectionsModal, }: PropsType): JSX.Element => { const [page, setPage] = useState(Page.SendStory); @@ -192,27 +198,61 @@ export const SendStoryModal = ({ Array >([]); - const [myStoriesPrivacy, setMyStoriesPrivacy] = useState( - MyStoriesPrivacy.AllSignalConnections - ); - const [myStoriesPrivacyUuids, setMyStoriesPrivacyUuids] = useState< - Set - >(new Set()); + const [confirmRemoveGroupId, setConfirmRemoveGroupId] = useState< + string | undefined + >(); + const [confirmDeleteListId, setConfirmDeleteListId] = useState< + string | undefined + >(); - const myStories = useMemo(() => { - return { + const [listIdToEdit, setListIdToEdit] = useState(); + + 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, id: MY_STORIES_ID, - name: MY_STORIES_ID, - isBlockList: myStoriesPrivacy !== MyStoriesPrivacy.OnlyShareWith, - members: - myStoriesPrivacy === MyStoriesPrivacy.AllSignalConnections - ? [] - : candidateConversations.filter( - convo => convo.uuid && myStoriesPrivacyUuids.has(convo.uuid) - ), - }; - }, [candidateConversations, myStoriesPrivacy, myStoriesPrivacyUuids]); + name: i18n('Stories__mine'), + isBlockList: ogMyStories?.isBlockList ?? true, + members: ogMyStories?.members || [], + }), + [i18n, ogMyStories] + ); + + const initialMyStoriesMemberUuids = useMemo( + () => (ogMyStories?.members || []).map(({ uuid }) => uuid).filter(isNotNil), + [ogMyStories] + ); + + const [stagedMyStories, setStagedMyStories] = + useState(initialMyStories); + + const [stagedMyStoriesMemberUuids, setStagedMyStoriesMemberUuids] = useState< + Array + >(initialMyStoriesMemberUuids); let content: JSX.Element; if (page === Page.SetMyStoriesPrivacy) { @@ -221,28 +261,63 @@ export const SendStoryModal = ({ hasDisclaimerAbove i18n={i18n} learnMore="SendStoryModal__privacy-disclaimer" - myStories={myStories} + myStories={stagedMyStories} onClickExclude={() => { - setMyStoriesPrivacy(MyStoriesPrivacy.Exclude); - setMyStoriesPrivacyUuids(new Set()); - setSelectedContacts([]); + let nextSelectedContacts = stagedMyStories.members; + + if (!stagedMyStories.isBlockList) { + setStagedMyStories(myStories => ({ + ...myStories, + isBlockList: true, + members: [], + })); + nextSelectedContacts = []; + } + + setSelectedContacts(nextSelectedContacts); + setPage(Page.HideStoryFrom); }} onClickOnlyShareWith={() => { - setMyStoriesPrivacy(MyStoriesPrivacy.OnlyShareWith); - setMyStoriesPrivacyUuids(new Set()); - setSelectedContacts([]); + if (!stagedMyStories.isBlockList) { + setSelectedContacts(stagedMyStories.members); + } else { + setStagedMyStories(myStories => ({ + ...myStories, + isBlockList: false, + members: [], + })); + } + setPage(Page.AddViewer); }} setSelectedContacts={setSelectedContacts} setMyStoriesToAllSignalConnections={() => { - setMyStoriesPrivacy(MyStoriesPrivacy.AllSignalConnections); - setMyStoriesPrivacyUuids(new Set()); + setStagedMyStories(myStories => ({ + ...myStories, + isBlockList: true, + members: [], + })); setSelectedContacts([]); }} toggleSignalConnectionsModal={toggleSignalConnectionsModal} /> ); + } else if (page === Page.EditingDistributionList && listToEdit) { + content = ( + + ); } else if ( page === Page.ChooseViewers || page === Page.NameStory || @@ -254,15 +329,21 @@ export const SendStoryModal = ({ candidateConversations={candidateConversations} getPreferredBadge={getPreferredBadge} i18n={i18n} - onDone={(name, uuids) => { + onCreateList={(name, uuids) => { onDistributionListCreated(name, uuids); setPage(Page.SendStory); }} 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); + } else if (listIdToEdit && page === Page.HideStoryFrom) { + onHideMyStoriesFrom(uuids); + setPage(Page.SendStory); } else if (page === Page.HideStoryFrom || page === Page.AddViewer) { - setMyStoriesPrivacyUuids(new Set(uuids)); + setStagedMyStoriesMemberUuids(uuids); setPage(Page.SetMyStoriesPrivacy); } else { setPage(Page.SendStory); @@ -415,7 +496,38 @@ export const SendStoryModal = ({ }} > {({ id, checkboxNode }) => ( - <> + 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} + > {checkboxNode} - + )} ))} @@ -484,7 +596,23 @@ export const SendStoryModal = ({ }} > {({ id, checkboxNode }) => ( - <> + setConfirmRemoveGroupId(group.id), + }, + ]} + moduleClassName="SendStoryModal__distribution-list-context" + onClick={noop} + popperOptions={{ + placement: 'bottom', + strategy: 'absolute', + }} + theme={Theme.Dark} + > {checkboxNode} - + )} ))} @@ -568,7 +696,7 @@ export const SendStoryModal = ({ className="SendStoryModal__ok" disabled={!chosenGroupIds.size} onClick={() => { - tagGroupsAsNewGroupStory(Array.from(chosenGroupIds)); + toggleGroupsForStorySend(Array.from(chosenGroupIds)); setChosenGroupIds(new Set()); setPage(Page.SendStory); }} @@ -598,21 +726,16 @@ export const SendStoryModal = ({ - - {listToEdit.members.map(member => ( -
- - - - {member.title} - - - -
- ))} - - )} - -
- -
- {i18n('StoriesSettings__replies-reactions--title')} -
- - onRepliesNReactionsChanged(listToEdit.id, value)} - /> - - {!isMyStories && ( - <> -
- - - - )} - + ); } else { const privateStories = distributionLists.filter( @@ -447,6 +316,182 @@ export const StoriesSettingsModal = ({ {i18n('StoriesSettings__delete-list--confirm')} )} + + ); +}; + +type DistributionListSettingsPropsType = { + i18n: LocalizerType; + listToEdit: StoryDistributionListWithMembersDataType; + setConfirmDeleteListId: (id: string) => unknown; + setPage: (page: Page) => unknown; + setSelectedContacts: (contacts: Array) => 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 && ( + <> +
+ + + + + + +
+ +
+ + )} + +
+ {i18n('StoriesSettings__who-can-see')} +
+ + {isMyStories && ( + { + setPage(Page.HideStoryFrom); + }} + onClickOnlyShareWith={() => { + setPage(Page.AddViewer); + }} + setSelectedContacts={setSelectedContacts} + setMyStoriesToAllSignalConnections={ + setMyStoriesToAllSignalConnections + } + toggleSignalConnectionsModal={toggleSignalConnectionsModal} + /> + )} + + {!isMyStories && ( + <> + + + {listToEdit.members.map(member => ( +
+ + + + {member.title} + + + +
+ ))} + + )} + +
+ +
+ {i18n('StoriesSettings__replies-reactions--title')} +
+ + onRepliesNReactionsChanged(listToEdit.id, value)} + /> + + {!isMyStories && ( + <> +
+ + + + )} + {confirmRemoveMember && ( ) => unknown; + onCreateList: (name: string, viewerUuids: Array) => unknown; onViewersUpdated: (viewerUuids: Array) => unknown; page: Page; selectedContacts: Array; @@ -592,7 +637,7 @@ export const EditDistributionList = ({ candidateConversations, getPreferredBadge, i18n, - onDone, + onCreateList, onViewersUpdated, page, selectedContacts, @@ -716,7 +761,7 @@ export const EditDistributionList = ({