Story settings modal copy and design changes

This commit is contained in:
Jamie Kyle 2022-10-26 16:17:39 -07:00 committed by GitHub
parent 7f0a66847b
commit 5ccfabeb0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 295 additions and 93 deletions

View file

@ -5388,7 +5388,7 @@
"description": "Label of the button on the bottom of the media editor that trigger the add-caption dialog" "description": "Label of the button on the bottom of the media editor that trigger the add-caption dialog"
}, },
"MyStories__title": { "MyStories__title": {
"message": "My Stories", "message": "My Story",
"description": "Title for the my stories list" "description": "Title for the my stories list"
}, },
"MyStories__story": { "MyStories__story": {
@ -5460,7 +5460,7 @@
"description": "Title for the stories list" "description": "Title for the stories list"
}, },
"Stories__mine": { "Stories__mine": {
"message": "My Stories", "message": "My Story",
"description": "Label for your stories" "description": "Label for your stories"
}, },
"Stories__add": { "Stories__add": {
@ -5515,6 +5515,10 @@
"message": "Story settings", "message": "Story settings",
"description": "Title for the story settings modal" "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"
},
"StoriesSettings__new-list": { "StoriesSettings__new-list": {
"message": "New custom story", "message": "New custom story",
"description": "Label to create a new custom distribution list" "description": "Label to create a new custom distribution list"
@ -5600,7 +5604,7 @@
"description": "Description of button StoriesSettings__mine__all--label" "description": "Description of button StoriesSettings__mine__all--label"
}, },
"StoriesSettings__mine__exclude--label": { "StoriesSettings__mine__exclude--label": {
"message": "All Signal connections except...", "message": "All except...",
"description": "Input label to create a block list" "description": "Input label to create a block list"
}, },
"StoriesSettings__mine__exclude--description": { "StoriesSettings__mine__exclude--description": {
@ -5704,8 +5708,8 @@
"description": "Modal title when choosing groups" "description": "Modal title when choosing groups"
}, },
"SendStoryModal__my-stories-privacy": { "SendStoryModal__my-stories-privacy": {
"message": "My stories privacy", "message": "My Story privacy",
"description": "Modal title for setting privacy for My Stories" "description": "Modal title for setting privacy for My Story"
}, },
"SendStoryModal__privacy-disclaimer": { "SendStoryModal__privacy-disclaimer": {
"message": "Choose which Signal connections can view your story. You can always change this in privacy settings. $learnMore$", "message": "Choose which Signal connections can view your story. You can always change this in privacy settings. $learnMore$",

View file

@ -74,7 +74,7 @@
@mixin font-subtitle { @mixin font-subtitle {
@include font-family; @include font-family;
font-size: 11px; font-size: 12px;
line-height: 16px; line-height: 16px;
letter-spacing: 0; letter-spacing: 0;
} }

View file

@ -74,9 +74,9 @@
@include font-body-1; @include font-body-1;
align-items: center; align-items: center;
display: flex; display: flex;
height: 52px;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
padding: 8px 0;
&--no-pointer { &--no-pointer {
cursor: inherit; cursor: inherit;
@ -144,9 +144,9 @@
} }
&__divider { &__divider {
border-color: $color-gray-65;
border-style: solid;
width: 100%; width: 100%;
border: 0 solid $color-gray-65;
border-top-width: 1px;
} }
&__input__container { &__input__container {
@ -165,12 +165,51 @@
margin-top: 32px; margin-top: 32px;
} }
&__description {
@include font-subtitle;
color: $color-gray-25;
margin-top: 0px;
margin-bottom: 16px;
}
&__listHeader {
display: flex;
align-items: center;
}
&__listHeader__title {
flex: 1;
@include font-body-1-bold;
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 { &__delete-list {
@include button-reset; @include button-reset;
align-items: center; align-items: center;
color: $color-accent-red; color: $color-accent-red;
display: flex; display: flex;
height: 52px; padding: 8px 0;
width: 100%; width: 100%;
&::before { &::before {
@ -186,7 +225,22 @@
} }
&__checkbox { &__checkbox {
margin: 18px 0; margin: 14px 0;
}
&__checkbox-container {
flex: 1;
display: flex;
align-items: center;
}
&__checkbox-label {
flex: 1;
margin-right: 8px;
}
&__checkbox-description {
color: $color-gray-25;
} }
&__conversation-list { &__conversation-list {
@ -204,4 +258,15 @@
color: $color-gray-05; color: $color-gray-05;
} }
} }
&__stories-off-container {
display: flex;
gap: 16;
align-items: center;
}
&__stories-off-text {
flex: 1;
font-size: 12px;
}
} }

View file

@ -12,6 +12,7 @@ export type PropsType = {
id: string; id: string;
checkboxNode: JSX.Element; checkboxNode: JSX.Element;
labelNode: JSX.Element; labelNode: JSX.Element;
checked?: boolean;
}) => JSX.Element; }) => JSX.Element;
description?: string; description?: string;
disabled?: boolean; disabled?: boolean;
@ -65,7 +66,7 @@ export const Checkbox = ({
<div className={getClassName('')}> <div className={getClassName('')}>
<div className={getClassName('__container')}> <div className={getClassName('__container')}>
{children ? ( {children ? (
children({ id, checkboxNode, labelNode }) children({ id, checkboxNode, labelNode, checked })
) : ( ) : (
<> <>
{checkboxNode} {checkboxNode}

View file

@ -113,7 +113,7 @@ function getKeyForMyStoryType(list: StoryDistributionListWithMembersDataType) {
return 'StoriesSettings__mine__all--label'; return 'StoriesSettings__mine__all--label';
} }
function getListViewers( export function getListViewers(
list: StoryDistributionListWithMembersDataType, list: StoryDistributionListWithMembersDataType,
i18n: LocalizerType, i18n: LocalizerType,
signalConnections: Array<ConversationType> signalConnections: Array<ConversationType>

View file

@ -7,7 +7,10 @@ import React from 'react';
import type { PropsType } from './StoriesSettingsModal'; import type { PropsType } from './StoriesSettingsModal';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { StoriesSettingsModal } from './StoriesSettingsModal'; import { StoriesSettingsModal } from './StoriesSettingsModal';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import {
getDefaultConversation,
getDefaultGroup,
} from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import { import {
getMyStories, getMyStories,
@ -23,9 +26,15 @@ export default {
candidateConversations: { candidateConversations: {
defaultValue: Array.from(Array(100), () => getDefaultConversation()), defaultValue: Array.from(Array(100), () => getDefaultConversation()),
}, },
signalConnections: {
defaultValue: Array.from(Array(42), getDefaultConversation),
},
distributionLists: { distributionLists: {
defaultValue: [], defaultValue: [],
}, },
groupStories: {
defaultValue: Array.from(Array(2), getDefaultGroup),
},
getPreferredBadge: { action: true }, getPreferredBadge: { action: true },
hideStoriesSettings: { action: true }, hideStoriesSettings: { action: true },
i18n: { i18n: {
@ -43,6 +52,7 @@ export default {
onViewersUpdated: { action: true }, onViewersUpdated: { action: true },
setMyStoriesToAllSignalConnections: { action: true }, setMyStoriesToAllSignalConnections: { action: true },
toggleSignalConnectionsModal: { action: true }, toggleSignalConnectionsModal: { action: true },
setStoriesDisabled: { action: true },
}, },
} as Meta; } as Meta;

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { MeasuredComponentProps } from 'react-measure'; import type { MeasuredComponentProps } from 'react-measure';
import type { ReactNode } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import Measure from 'react-measure'; import Measure from 'react-measure';
import { noop } from 'lodash'; import { noop } from 'lodash';
@ -36,10 +37,12 @@ import {
asyncShouldNeverBeCalled, asyncShouldNeverBeCalled,
} from '../util/shouldNeverBeCalled'; } from '../util/shouldNeverBeCalled';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
import { getListViewers } from './SendStoryModal';
export type PropsType = { export type PropsType = {
candidateConversations: Array<ConversationType>; candidateConversations: Array<ConversationType>;
distributionLists: Array<StoryDistributionListWithMembersDataType>; distributionLists: Array<StoryDistributionListWithMembersDataType>;
signalConnections: Array<ConversationType>;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
hideStoriesSettings: () => unknown; hideStoriesSettings: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
@ -62,6 +65,8 @@ export type PropsType = {
setMyStoriesToAllSignalConnections: () => unknown; setMyStoriesToAllSignalConnections: () => unknown;
storyViewReceiptsEnabled: boolean; storyViewReceiptsEnabled: boolean;
toggleSignalConnectionsModal: () => unknown; toggleSignalConnectionsModal: () => unknown;
toggleStoriesView: () => void;
setStoriesDisabled: (value: boolean) => void;
}; };
export enum Page { export enum Page {
@ -89,9 +94,65 @@ const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'moduleClassName'> =
moduleClassName: 'StoriesSettingsModal__modal', moduleClassName: 'StoriesSettingsModal__modal',
}; };
type DistributionListItemProps = {
i18n: LocalizerType;
distributionList: StoryDistributionListWithMembersDataType;
me: ConversationType;
signalConnections: Array<ConversationType>;
onSelectItemToEdit(id: UUIDStringType): void;
};
function DistributionListItem({
i18n,
distributionList,
me,
signalConnections,
onSelectItemToEdit,
}: DistributionListItemProps) {
return (
<button
className="StoriesSettingsModal__list"
onClick={() => {
onSelectItemToEdit(distributionList.id);
}}
type="button"
>
<span className="StoriesSettingsModal__list__left">
{distributionList.id === MY_STORIES_ID ? (
<Avatar
acceptedMessageRequest={me.acceptedMessageRequest}
avatarPath={me.avatarPath}
badge={undefined}
color={me.color}
conversationType={me.type}
i18n={i18n}
isMe
sharedGroupNames={me.sharedGroupNames}
size={AvatarSize.THIRTY_SIX}
title={me.title}
/>
) : (
<span className="StoriesSettingsModal__list__avatar--private" />
)}
<span className="StoriesSettingsModal__list__title">
<StoryDistributionListName
i18n={i18n}
id={distributionList.id}
name={distributionList.name}
/>
</span>
</span>
<span className="StoriesSettingsModal__list__viewers">
{getListViewers(distributionList, i18n, signalConnections)}
</span>
</button>
);
}
export const StoriesSettingsModal = ({ export const StoriesSettingsModal = ({
candidateConversations, candidateConversations,
distributionLists, distributionLists,
signalConnections,
getPreferredBadge, getPreferredBadge,
hideStoriesSettings, hideStoriesSettings,
i18n, i18n,
@ -105,6 +166,8 @@ export const StoriesSettingsModal = ({
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
storyViewReceiptsEnabled, storyViewReceiptsEnabled,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
toggleStoriesView,
setStoriesDisabled,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
@ -200,10 +263,6 @@ export const StoriesSettingsModal = ({
/> />
); );
} else { } else {
const privateStories = distributionLists.filter(
list => list.id !== MY_STORIES_ID
);
modal = onClose => ( modal = onClose => (
<ModalPage <ModalPage
modalName="StoriesSettingsModal__list" modalName="StoriesSettingsModal__list"
@ -212,72 +271,36 @@ export const StoriesSettingsModal = ({
title={i18n('StoriesSettings__title')} title={i18n('StoriesSettings__title')}
{...modalCommonProps} {...modalCommonProps}
> >
<button <p className="StoriesSettingsModal__description">
className="StoriesSettingsModal__list" {i18n('icu:StoriesSettings__description')}
onClick={() => { </p>
setListToEditId(MY_STORIES_ID);
}}
type="button"
>
<span className="StoriesSettingsModal__list__left">
<Avatar
acceptedMessageRequest={me.acceptedMessageRequest}
avatarPath={me.avatarPath}
badge={getPreferredBadge(me.badges)}
color={me.color}
conversationType={me.type}
i18n={i18n}
isMe
sharedGroupNames={me.sharedGroupNames}
size={AvatarSize.THIRTY_SIX}
theme={ThemeType.dark}
title={me.title}
/>
<span className="StoriesSettingsModal__list__title">
{i18n('Stories__mine')}
</span>
</span>
<span className="StoriesSettingsModal__list__viewers" /> <div className="StoriesSettingsModal__listHeader">
</button> <h2 className="StoriesSettingsModal__listHeader__title">
{i18n('Stories__mine')}
<button </h2>
className="StoriesSettingsModal__list"
onClick={() => {
setPage(Page.ChooseViewers);
}}
type="button"
>
<span className="StoriesSettingsModal__list__left">
<span className="StoriesSettingsModal__list__avatar--new" />
<span className="StoriesSettingsModal__list__title">
{i18n('StoriesSettings__new-list')}
</span>
</span>
</button>
{privateStories.map(list => (
<button <button
className="StoriesSettingsModal__list"
key={list.id}
onClick={() => {
setListToEditId(list.id);
}}
type="button" type="button"
className="StoriesSettingsModal__listHeader__button"
onClick={() => {
setPage(Page.ChooseViewers);
}}
> >
<span className="StoriesSettingsModal__list__left"> {i18n('StoriesSettings__new-list')}
<span className="StoriesSettingsModal__list__avatar--custom" />
<span className="StoriesSettingsModal__list__title">
{list.name}
</span>
</span>
<span className="StoriesSettingsModal__list__viewers">
{i18n('icu:StoriesSettings__viewers', {
count: list.members.length,
})}
</span>
</button> </button>
))} </div>
{distributionLists.map(distributionList => {
return (
<DistributionListItem
i18n={i18n}
me={me}
distributionList={distributionList}
signalConnections={signalConnections}
onSelectItemToEdit={setListToEditId}
/>
);
})}
<hr className="StoriesSettingsModal__divider" /> <hr className="StoriesSettingsModal__divider" />
@ -290,6 +313,22 @@ export const StoriesSettingsModal = ({
name="view-receipts" name="view-receipts"
onChange={noop} onChange={noop}
/> />
<div className="StoriesSettingsModal__stories-off-container">
<p className="StoriesSettingsModal__stories-off-text">
{i18n('Preferences__turn-stories-off--body')}
</p>
<Button
className="Preferences__stories-off"
onClick={async () => {
setStoriesDisabled(true);
toggleStoriesView();
onClose();
}}
>
{i18n('Preferences__turn-stories-off')}
</Button>
</div>
</ModalPage> </ModalPage>
); );
} }
@ -555,6 +594,30 @@ export const DistributionListSettingsModal = ({
); );
}; };
type CheckboxRenderProps = {
checkboxNode: ReactNode;
labelNode: ReactNode;
descriptionNode: ReactNode;
};
function CheckboxRender({
checkboxNode,
labelNode,
descriptionNode,
}: CheckboxRenderProps) {
return (
<>
{checkboxNode}
<div className="StoriesSettingsModal__checkbox-container">
<div className="StoriesSettingsModal__checkbox-label">{labelNode}</div>
<div className="StoriesSettingsModal__checkbox-description">
{descriptionNode}
</div>
</div>
</>
);
}
type EditMyStoriesPrivacyPropsType = { type EditMyStoriesPrivacyPropsType = {
hasDisclaimerAbove?: boolean; hasDisclaimerAbove?: boolean;
i18n: LocalizerType; i18n: LocalizerType;
@ -605,7 +668,6 @@ export const EditMyStoriesPrivacy = ({
<Checkbox <Checkbox
checked={!myStories.members.length} checked={!myStories.members.length}
description={i18n('StoriesSettings__mine__all--description')}
isRadio isRadio
label={i18n('StoriesSettings__mine__all--label')} label={i18n('StoriesSettings__mine__all--label')}
moduleClassName="StoriesSettingsModal__checkbox" moduleClassName="StoriesSettingsModal__checkbox"
@ -613,13 +675,28 @@ export const EditMyStoriesPrivacy = ({
onChange={() => { onChange={() => {
setMyStoriesToAllSignalConnections(); setMyStoriesToAllSignalConnections();
}} }}
/> >
{({ checkboxNode, labelNode, checked }) => {
return (
<CheckboxRender
checkboxNode={checkboxNode}
labelNode={labelNode}
descriptionNode={
checked && (
<>
{i18n('icu:StoriesSettings__viewers', {
count: myStories.members.length,
})}
</>
)
}
/>
);
}}
</Checkbox>
<Checkbox <Checkbox
checked={myStories.isBlockList && myStories.members.length > 0} checked={myStories.isBlockList && myStories.members.length > 0}
description={i18n('StoriesSettings__mine__exclude--description', [
myStories.isBlockList ? String(myStories.members.length) : '0',
])}
isRadio isRadio
label={i18n('StoriesSettings__mine__exclude--label')} label={i18n('StoriesSettings__mine__exclude--label')}
moduleClassName="StoriesSettingsModal__checkbox" moduleClassName="StoriesSettingsModal__checkbox"
@ -631,17 +708,28 @@ export const EditMyStoriesPrivacy = ({
} }
onClickExclude(); onClickExclude();
}} }}
/> >
{({ checkboxNode, labelNode, checked }) => {
return (
<CheckboxRender
checkboxNode={checkboxNode}
labelNode={labelNode}
descriptionNode={
checked && (
<>
{i18n('icu:StoriesSettings__viewers', {
count: myStories.members.length,
})}
</>
)
}
/>
);
}}
</Checkbox>
<Checkbox <Checkbox
checked={!myStories.isBlockList && myStories.members.length > 0} checked={!myStories.isBlockList && myStories.members.length > 0}
description={
!myStories.isBlockList && myStories.members.length
? i18n('StoriesSettings__mine__only--description--people', [
String(myStories.members.length),
])
: i18n('StoriesSettings__mine__only--description')
}
isRadio isRadio
label={i18n('StoriesSettings__mine__only--label')} label={i18n('StoriesSettings__mine__only--label')}
moduleClassName="StoriesSettingsModal__checkbox" moduleClassName="StoriesSettingsModal__checkbox"
@ -653,7 +741,25 @@ export const EditMyStoriesPrivacy = ({
} }
onClickOnlyShareWith(); onClickOnlyShareWith();
}} }}
/> >
{({ checkboxNode, labelNode, checked }) => {
return (
<CheckboxRender
checkboxNode={checkboxNode}
labelNode={labelNode}
descriptionNode={
checked && (
<>
{i18n('icu:StoriesSettings__viewers', {
count: myStories.members.length,
})}
</>
)
}
/>
);
}}
</Checkbox>
{!hasDisclaimerAbove && disclaimerElement} {!hasDisclaimerAbove && disclaimerElement}
</> </>

View file

@ -1130,6 +1130,14 @@ const viewStory: ViewStoryActionCreatorType = (
}; };
}; };
function setStoriesDisabled(
value: boolean
): ThunkAction<void, RootStateType, unknown, never> {
return async () => {
await window.Events.setHasStoriesDisabled(value);
};
}
export const actions = { export const actions = {
deleteStoryForEveryone, deleteStoryForEveryone,
loadStoryReplies, loadStoryReplies,
@ -1145,6 +1153,7 @@ export const actions = {
verifyStoryListMembers, verifyStoryListMembers,
viewUserStories, viewUserStories,
viewStory, viewStory,
setStoriesDisabled,
}; };
export const useStoriesActions = (): typeof actions => useBoundActions(actions); export const useStoriesActions = (): typeof actions => useBoundActions(actions);

View file

@ -8,6 +8,7 @@ import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { StoriesSettingsModal } from '../../components/StoriesSettingsModal'; import { StoriesSettingsModal } from '../../components/StoriesSettingsModal';
import { import {
getAllSignalConnections,
getCandidateContactsForNewGroup, getCandidateContactsForNewGroup,
getMe, getMe,
} from '../selectors/conversations'; } from '../selectors/conversations';
@ -17,8 +18,10 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import { getHasStoryViewReceiptSetting } from '../selectors/items'; import { getHasStoryViewReceiptSetting } from '../selectors/items';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists'; import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
import { useStoriesActions } from '../ducks/stories';
export function SmartStoriesSettingsModal(): JSX.Element | null { export function SmartStoriesSettingsModal(): JSX.Element | null {
const { toggleStoriesView, setStoriesDisabled } = useStoriesActions();
const { hideStoriesSettings, toggleSignalConnectionsModal } = const { hideStoriesSettings, toggleSignalConnectionsModal } =
useGlobalModalActions(); useGlobalModalActions();
const { const {
@ -30,6 +33,7 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
updateStoryViewers, updateStoryViewers,
} = useStoryDistributionListsActions(); } = useStoryDistributionListsActions();
const signalConnections = useSelector(getAllSignalConnections);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting); const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting);
@ -43,6 +47,7 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
<StoriesSettingsModal <StoriesSettingsModal
candidateConversations={candidateConversations} candidateConversations={candidateConversations}
distributionLists={distributionLists} distributionLists={distributionLists}
signalConnections={signalConnections}
hideStoriesSettings={hideStoriesSettings} hideStoriesSettings={hideStoriesSettings}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
@ -56,6 +61,8 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections} setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
storyViewReceiptsEnabled={storyViewReceiptsEnabled} storyViewReceiptsEnabled={storyViewReceiptsEnabled}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
toggleStoriesView={toggleStoriesView}
setStoriesDisabled={setStoriesDisabled}
/> />
); );
} }