diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6af86609c4..1ba5c23a63 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -5512,21 +5512,37 @@ "description": "Toast message" }, "StoriesSettings__title": { - "message": "Story settings", + "message": "Story privacy", "description": "Title for the story settings modal" }, "icu:StoriesSettings__description": { "messageformat": "Stories automatically disapper after 24 hours. Choose who can view your story or create new stories with specific viewers or groups.", "description": "Description for story settings modal" }, + "icu:StoriesSettings__my_stories": { + "messageformat": "My Stories", + "description": "Title of distribution lists section in stories settings modal" + }, "StoriesSettings__new-list": { - "message": "New custom story", + "message": "New Story", "description": "Label to create a new custom distribution list" }, "StoriesSettings__new-list--visibility": { "message": "Only you can see the name of this story.", "description": "Explanation about the visibility of custom distribution list names" }, + "icu:StoriesSettings__my-story-subtitle": { + "messageformat": "All Signal connections", + "description": "Story settings modal my story distribution list selection subtitle" + }, + "icu:StoriesSettings__custom-story-subtitle": { + "messageformat": "Custom story", + "description": "Story settings modal custom story distribution list selection subtitle" + }, + "icu:StoriesSettings__group-story-subtitle": { + "messageformat": "Group story", + "description": "Story settings modal group story selection subtitle" + }, "StoriesSettings__viewers--singular": { "message": "$num$ viewer", "description": "(deleted 2022/10/21) A single viewer" @@ -5643,6 +5659,22 @@ "message": "To change this setting, open the Signal app on your mobile device and navigate to Settings -> Stories", "description": "Description of how view receipts can be changed in story settings" }, + "icu:GroupStorySettingsModal__members_title": { + "messageformat": "Who can view this story", + "description": "Stories settings > Group Story > members list title" + }, + "icu:GroupStorySettingsModal__members_help": { + "messageformat": "Members of the group chat “{groupTitle}” can view and reply to this story. You can update the membership for this chat in the group.", + "description": "Stories settings > Group Story > group story help text" + }, + "icu:GroupStorySettingsModal__remove_group": { + "messageformat": "Remove group story", + "descirption": "Stories settings > Group Story > button to remove group story" + }, + "icu:StoriesSettings__remove_group--confirm": { + "messageformat": "Are you sure you want to remove “{groupTitle}”?", + "descirption": "Stories settings > Group Story > confirm to remove group story" + }, "SendStoryModal__choose-who-can-view": { "message": "Choose who can view your story", "description": "Shown during the first time posting a story" @@ -5732,9 +5764,13 @@ "description": "Select box title for the stories on/off toggle" }, "Stories__settings-toggle--description": { - "message": "You will no longer be able to share or view Stories when this option is turned off.", + "message": "If you opt out of stories you will no longer be able to share or view stories.", "description": "Select box description for the stories on/off toggle" }, + "Stories__settings-toggle--button": { + "message": "Turn off stories", + "description": "Button to turn off stories in stories settings modal" + }, "StoryViewer__pause": { "message": "Pause", "description": "Aria label for pausing a story" diff --git a/images/icons/v2/disable-outline-20.svg b/images/icons/v2/disable-outline-20.svg new file mode 100644 index 0000000000..f99d6038c3 --- /dev/null +++ b/images/icons/v2/disable-outline-20.svg @@ -0,0 +1,5 @@ + + + diff --git a/stylesheets/components/StoriesSettingsModal.scss b/stylesheets/components/StoriesSettingsModal.scss index 080bd58af1..b3db8f9f60 100644 --- a/stylesheets/components/StoriesSettingsModal.scss +++ b/stylesheets/components/StoriesSettingsModal.scss @@ -2,6 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-only .StoriesSettingsModal { + &__width-container { + max-width: 420px; + } + + &__modal { + width: 420px; + } + &__conversation-list { .module-conversation-list { padding-left: 0; @@ -83,6 +91,8 @@ } &__viewers { + display: flex; + @include font-body-2; color: $color-gray-25; } @@ -183,27 +193,6 @@ margin-right: 8px; } - &__listHeader__button { - @include button-reset; - @include button-secondary; - @include rounded-corners; - padding: 4px 10px; - @include font-family; - font-size: 12px; - display: flex; - align-items: center; - - &::before { - content: ''; - display: inline-block; - width: 12px; - height: 12px; - flex-shrink: 0; - margin-right: 6px; - @include color-svg('../images/icons/v2/plus-24.svg', $color-white); - } - } - &__delete-list { @include button-reset; align-items: center; @@ -261,12 +250,62 @@ &__stories-off-container { display: flex; - gap: 16; + gap: 16px; align-items: center; } &__stories-off-text { flex: 1; - font-size: 12px; + color: $color-gray-25; + @include font-subtitle; + } +} + +.GroupStorySettingsModal { + &__header { + display: flex; + gap: 12px; + align-items: center; + padding-bottom: 12px; + } + + &__members_title { + @include font-body-1-bold; + } + + &__members_item { + display: flex; + gap: 12px; + align-items: center; + + &__name { + @include font-body-1; + } + } + + &__members_help { + @include font-body-2; + color: $color-gray-25; + } + + &__remove_group { + @include button-reset; + align-items: center; + color: $color-accent-red; + display: flex; + padding: 8px 0; + width: 100%; + margin-top: 12px; + + &::before { + @include color-svg( + '../images/icons/v2/disable-outline-20.svg', + $color-accent-red + ); + content: ''; + height: 20px; + margin-right: 20px; + width: 20px; + } } } diff --git a/ts/components/StoriesSettingsModal.stories.tsx b/ts/components/StoriesSettingsModal.stories.tsx index 8afd1eb1ba..5fd89b021a 100644 --- a/ts/components/StoriesSettingsModal.stories.tsx +++ b/ts/components/StoriesSettingsModal.stories.tsx @@ -45,6 +45,7 @@ export default { }, storyViewReceiptsEnabled: { control: 'boolean' }, onDeleteList: { action: true }, + toggleGroupsForStorySend: { action: true }, onDistributionListCreated: { action: true }, onHideMyStoriesFrom: { action: true }, onRemoveMember: { action: true }, @@ -53,6 +54,9 @@ export default { setMyStoriesToAllSignalConnections: { action: true }, toggleSignalConnectionsModal: { action: true }, setStoriesDisabled: { action: true }, + getConversationByUuid: { + defaultValue: () => getDefaultGroup(), + }, }, } as Meta; diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx index ca98654e3f..0936214dfe 100644 --- a/ts/components/StoriesSettingsModal.tsx +++ b/ts/components/StoriesSettingsModal.tsx @@ -37,16 +37,19 @@ import { asyncShouldNeverBeCalled, } from '../util/shouldNeverBeCalled'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; +import { getGroupMemberships } from '../util/getGroupMemberships'; export type PropsType = { candidateConversations: Array; distributionLists: Array; + groupStories: Array; signalConnections: Array; getPreferredBadge: PreferredBadgeSelectorType; hideStoriesSettings: () => unknown; i18n: LocalizerType; me: ConversationType; onDeleteList: (listId: string) => unknown; + toggleGroupsForStorySend: (groupIds: Array) => unknown; onDistributionListCreated: ( name: string, viewerUuids: Array @@ -66,6 +69,7 @@ export type PropsType = { toggleSignalConnectionsModal: () => unknown; toggleStoriesView: () => void; setStoriesDisabled: (value: boolean) => void; + getConversationByUuid: (uuid: UUIDStringType) => ConversationType | undefined; }; export enum Page { @@ -124,6 +128,7 @@ function DistributionListItem({ signalConnections, onSelectItemToEdit, }: DistributionListItemProps) { + const isMyStories = distributionList.id === MY_STORIES_ID; return ( + ); +} + +type GroupStoryItemProps = { + i18n: LocalizerType; + groupStory: ConversationType; + onSelectGroupToView(id: string): void; +}; + +function GroupStoryItem({ + i18n, + groupStory, + onSelectGroupToView, +}: GroupStoryItemProps) { + return ( + ); @@ -167,12 +223,14 @@ function DistributionListItem({ export const StoriesSettingsModal = ({ candidateConversations, distributionLists, + groupStories, signalConnections, getPreferredBadge, hideStoriesSettings, i18n, me, onDeleteList, + toggleGroupsForStorySend, onDistributionListCreated, onHideMyStoriesFrom, onRemoveMember, @@ -183,6 +241,7 @@ export const StoriesSettingsModal = ({ toggleSignalConnectionsModal, toggleStoriesView, setStoriesDisabled, + getConversationByUuid, }: PropsType): JSX.Element => { const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); @@ -195,6 +254,13 @@ export const StoriesSettingsModal = ({ [distributionLists, listToEditId] ); + const [groupToViewId, setGroupToViewId] = useState(null); + const groupToView = useMemo(() => { + return groupStories.find(group => { + return group.id === groupToViewId; + }); + }, [groupStories, groupToViewId]); + const [page, setPage] = useState(Page.DistributionLists); const [selectedContacts, setSelectedContacts] = useState< @@ -210,6 +276,11 @@ export const StoriesSettingsModal = ({ { id: string; name: string } | undefined >(); + const [confirmRemoveGroup, setConfirmRemoveGroup] = useState<{ + id: string; + title: string; + } | null>(null); + let modal: RenderModalPage | null; if (page !== Page.DistributionLists) { @@ -237,6 +308,8 @@ export const StoriesSettingsModal = ({ resetChooseViewersScreen(); } else if (listToEdit) { setListToEditId(undefined); + } else if (groupToView) { + setGroupToViewId(null); } }) } @@ -277,6 +350,22 @@ export const StoriesSettingsModal = ({ onClose={handleClose} /> ); + } else if (groupToView) { + modal = onClose => ( + setGroupToViewId(null)} + getConversationByUuid={getConversationByUuid} + onRemoveGroup={group => { + setConfirmRemoveGroup({ + id: group.id, + title: group.title, + }); + }} + /> + ); } else { modal = onClose => (

- {i18n('Stories__mine')} + {i18n('icu:StoriesSettings__my_stories')}

- + + {distributionLists.map(distributionList => { return ( { + return ( + + ); + })} +

- {i18n('Preferences__turn-stories-off--body')} + {i18n('Stories__settings-toggle--description')}

@@ -353,6 +461,7 @@ export const StoriesSettingsModal = ({ {!confirmDiscardModal && ( confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings) @@ -385,6 +494,31 @@ export const StoriesSettingsModal = ({ ])} )} + {confirmRemoveGroup != null && ( + { + toggleGroupsForStorySend([confirmRemoveGroup.id]); + setConfirmRemoveGroup(null); + setGroupToViewId(null); + }, + style: 'negative', + text: i18n('delete'), + }, + ]} + i18n={i18n} + onClose={() => { + setConfirmRemoveGroup(null); + }} + theme={Theme.Dark} + > + {i18n('icu:StoriesSettings__remove_group--confirm', { + groupTitle: confirmRemoveGroup.title, + })} + + )} {confirmDiscardModal} ); @@ -1071,3 +1205,96 @@ export const EditDistributionListModal = ({ ); }; + +type GroupStorySettingsModalProps = { + i18n: LocalizerType; + group: ConversationType; + onClose(): void; + onBackButtonClick(): void; + getConversationByUuid(uuid: UUIDStringType): ConversationType | undefined; + onRemoveGroup(group: ConversationType): void; +}; + +export const GroupStorySettingsModal = ({ + i18n, + group, + onClose, + onBackButtonClick, + getConversationByUuid, + onRemoveGroup, +}: GroupStorySettingsModalProps): JSX.Element => { + const groupMemberships = getGroupMemberships(group, getConversationByUuid); + return ( + +
+ + {group.title} +
+ +
+ +

+ {i18n('icu:GroupStorySettingsModal__members_title')} +

+ {groupMemberships.memberships.map(membership => { + const { member } = membership; + return ( +
+ +

+ {member.title} +

+
+ ); + })} + +

+ {i18n('icu:GroupStorySettingsModal__members_help', { + groupTitle: group.title, + })} +

+ +
+ + +
+ ); +}; diff --git a/ts/state/smart/StoriesSettingsModal.tsx b/ts/state/smart/StoriesSettingsModal.tsx index 506f1e5c4c..a07ae31a82 100644 --- a/ts/state/smart/StoriesSettingsModal.tsx +++ b/ts/state/smart/StoriesSettingsModal.tsx @@ -10,6 +10,8 @@ import { StoriesSettingsModal } from '../../components/StoriesSettingsModal'; import { getAllSignalConnections, getCandidateContactsForNewGroup, + getConversationByUuidSelector, + getGroupStories, getMe, } from '../selectors/conversations'; import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists'; @@ -19,6 +21,7 @@ import { getHasStoryViewReceiptSetting } from '../selectors/items'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists'; import { useStoriesActions } from '../ducks/stories'; +import { useConversationsActions } from '../ducks/conversations'; export function SmartStoriesSettingsModal(): JSX.Element | null { const { toggleStoriesView, setStoriesDisabled } = useStoriesActions(); @@ -33,6 +36,7 @@ export function SmartStoriesSettingsModal(): JSX.Element | null { setMyStoriesToAllSignalConnections, updateStoryViewers, } = useStoryDistributionListsActions(); + const { toggleGroupsForStorySend } = useConversationsActions(); const signalConnections = useSelector(getAllSignalConnections); const getPreferredBadge = useSelector(getPreferredBadgeSelector); @@ -42,17 +46,23 @@ export function SmartStoriesSettingsModal(): JSX.Element | null { const candidateConversations = useSelector(getCandidateContactsForNewGroup); const distributionLists = useSelector(getDistributionListsWithMembers); + const groupStories = useSelector(getGroupStories); + + const getConversationByUuid = useSelector(getConversationByUuidSelector); return ( ; + pendingApprovalMemberships: Array; + pendingMemberships: Array; +}; + export const getGroupMemberships = ( { memberships = [], @@ -22,11 +28,7 @@ export const getGroupMemberships = ( > >, getConversationByUuid: (uuid: UUIDStringType) => undefined | ConversationType -): { - memberships: Array; - pendingApprovalMemberships: Array; - pendingMemberships: Array; -} => ({ +): GroupMemberships => ({ memberships: memberships.reduce( (result: Array, membership) => { const member = getConversationByUuid(membership.uuid);