Update story settings modal design, add group details page

This commit is contained in:
Jamie Kyle 2022-11-01 10:34:23 -07:00 committed by GitHub
parent 8f62442822
commit 97b7f3dbc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 369 additions and 46 deletions

View file

@ -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"

View file

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10 19.375C15.1287 19.375 19.375 15.1195 19.375 10C19.375 4.87132 15.1195 0.625 9.99081 0.625C4.87132 0.625 0.625 4.87132 0.625 10C0.625 15.1195 4.88051 19.375 10 19.375ZM10 17.8125C5.66176 17.8125 2.19669 14.3382 2.19669 10C2.19669 5.66176 5.65257 2.1875 9.99081 2.1875C14.329 2.1875 17.8033 5.66176 17.8125 10C17.8217 14.3382 14.3382 17.8125 10 17.8125ZM6.38787 10.7812H13.6029C14.1176 10.7812 14.4669 10.5055 14.4669 10.0184C14.4669 9.52206 14.136 9.23713 13.6029 9.23713H6.38787C5.85478 9.23713 5.51471 9.52206 5.51471 10.0184C5.51471 10.5055 5.87316 10.7812 6.38787 10.7812Z"
fill="#F44336" />
</svg>

After

Width:  |  Height:  |  Size: 721 B

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -37,16 +37,19 @@ import {
asyncShouldNeverBeCalled,
} from '../util/shouldNeverBeCalled';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
import { getGroupMemberships } from '../util/getGroupMemberships';
export type PropsType = {
candidateConversations: Array<ConversationType>;
distributionLists: Array<StoryDistributionListWithMembersDataType>;
groupStories: Array<ConversationType>;
signalConnections: Array<ConversationType>;
getPreferredBadge: PreferredBadgeSelectorType;
hideStoriesSettings: () => unknown;
i18n: LocalizerType;
me: ConversationType;
onDeleteList: (listId: string) => unknown;
toggleGroupsForStorySend: (groupIds: Array<string>) => unknown;
onDistributionListCreated: (
name: string,
viewerUuids: Array<UUIDStringType>
@ -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 (
<button
className="StoriesSettingsModal__list"
@ -147,7 +152,7 @@ function DistributionListItem({
title={me.title}
/>
) : (
<span className="StoriesSettingsModal__list__avatar--private" />
<span className="StoriesSettingsModal__list__avatar--custom" />
)}
<span className="StoriesSettingsModal__list__title">
<StoryDistributionListName
@ -155,10 +160,61 @@ function DistributionListItem({
id={distributionList.id}
name={distributionList.name}
/>
<span className="StoriesSettingsModal__list__viewers">
{isMyStories
? i18n('icu:StoriesSettings__my-story-subtitle')
: i18n('icu:StoriesSettings__custom-story-subtitle')}
&nbsp;&middot;&nbsp;
{getListViewers(distributionList, i18n, signalConnections)}
</span>
</span>
</span>
<span className="StoriesSettingsModal__list__viewers">
{getListViewers(distributionList, i18n, signalConnections)}
</button>
);
}
type GroupStoryItemProps = {
i18n: LocalizerType;
groupStory: ConversationType;
onSelectGroupToView(id: string): void;
};
function GroupStoryItem({
i18n,
groupStory,
onSelectGroupToView,
}: GroupStoryItemProps) {
return (
<button
className="StoriesSettingsModal__list"
onClick={() => {
onSelectGroupToView(groupStory.id);
}}
type="button"
>
<span className="StoriesSettingsModal__list__left">
<Avatar
acceptedMessageRequest={groupStory.acceptedMessageRequest}
avatarPath={groupStory.avatarPath}
badge={undefined}
color={groupStory.color}
conversationType={groupStory.type}
i18n={i18n}
isMe={false}
sharedGroupNames={[]}
size={AvatarSize.THIRTY_SIX}
title={groupStory.title}
/>
<span className="StoriesSettingsModal__list__title">
{groupStory.title}
<span className="StoriesSettingsModal__list__viewers">
{i18n('icu:StoriesSettings__group-story-subtitle')}
&nbsp;&middot;&nbsp;
{i18n('icu:StoriesSettings__viewers', {
count: groupStory.membersCount,
})}
</span>
</span>
</span>
</button>
);
@ -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<string | null>(null);
const groupToView = useMemo(() => {
return groupStories.find(group => {
return group.id === groupToViewId;
});
}, [groupStories, groupToViewId]);
const [page, setPage] = useState<Page>(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 => (
<GroupStorySettingsModal
i18n={i18n}
group={groupToView}
onClose={onClose}
onBackButtonClick={() => setGroupToViewId(null)}
getConversationByUuid={getConversationByUuid}
onRemoveGroup={group => {
setConfirmRemoveGroup({
id: group.id,
title: group.title,
});
}}
/>
);
} else {
modal = onClose => (
<ModalPage
@ -292,22 +381,29 @@ export const StoriesSettingsModal = ({
<div className="StoriesSettingsModal__listHeader">
<h2 className="StoriesSettingsModal__listHeader__title">
{i18n('Stories__mine')}
{i18n('icu:StoriesSettings__my_stories')}
</h2>
<button
type="button"
className="StoriesSettingsModal__listHeader__button"
onClick={() => {
setPage(Page.ChooseViewers);
}}
>
{i18n('StoriesSettings__new-list')}
</button>
</div>
<button
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>
{distributionLists.map(distributionList => {
return (
<DistributionListItem
key={distributionList.id}
i18n={i18n}
me={me}
distributionList={distributionList}
@ -317,6 +413,17 @@ export const StoriesSettingsModal = ({
);
})}
{groupStories.map(groupStory => {
return (
<GroupStoryItem
key={groupStory.id}
i18n={i18n}
groupStory={groupStory}
onSelectGroupToView={setGroupToViewId}
/>
);
})}
<hr className="StoriesSettingsModal__divider" />
<Checkbox
@ -331,17 +438,18 @@ export const StoriesSettingsModal = ({
<div className="StoriesSettingsModal__stories-off-container">
<p className="StoriesSettingsModal__stories-off-text">
{i18n('Preferences__turn-stories-off--body')}
{i18n('Stories__settings-toggle--description')}
</p>
<Button
className="Preferences__stories-off"
variant={ButtonVariant.SecondaryDestructive}
onClick={async () => {
setStoriesDisabled(true);
toggleStoriesView();
onClose();
}}
>
{i18n('Preferences__turn-stories-off')}
{i18n('Stories__settings-toggle--button')}
</Button>
</div>
</ModalPage>
@ -353,6 +461,7 @@ export const StoriesSettingsModal = ({
{!confirmDiscardModal && (
<PagedModal
modalName="StoriesSettingsModal"
moduleClassName="StoriesSettingsModal"
theme={Theme.Dark}
onClose={() =>
confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings)
@ -385,6 +494,31 @@ export const StoriesSettingsModal = ({
])}
</ConfirmationDialog>
)}
{confirmRemoveGroup != null && (
<ConfirmationDialog
dialogName="StoriesSettings.removeGroupStory"
actions={[
{
action: () => {
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,
})}
</ConfirmationDialog>
)}
{confirmDiscardModal}
</>
);
@ -1071,3 +1205,96 @@ export const EditDistributionListModal = ({
</ModalPage>
);
};
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 (
<ModalPage
modalName="GroupStorySettingsModal"
i18n={i18n}
onClose={onClose}
onBackButtonClick={onBackButtonClick}
title={group.title}
{...modalCommonProps}
>
<div className="GroupStorySettingsModal__header">
<Avatar
acceptedMessageRequest={group.acceptedMessageRequest}
avatarPath={group.avatarPath}
badge={undefined}
color={group.color}
conversationType={group.type}
i18n={i18n}
isMe={false}
sharedGroupNames={[]}
size={AvatarSize.THIRTY_SIX}
title={group.title}
/>
<span className="GroupStorySettingsModal__title">{group.title}</span>
</div>
<hr className="StoriesSettingsModal__divider" />
<p className="GroupStorySettingsModal__members_title">
{i18n('icu:GroupStorySettingsModal__members_title')}
</p>
{groupMemberships.memberships.map(membership => {
const { member } = membership;
return (
<div
key={member.id}
className="GroupStorySettingsModal__members_item"
>
<Avatar
acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath}
badge={undefined}
color={member.color}
conversationType={member.type}
i18n={i18n}
isMe={false}
sharedGroupNames={[]}
size={AvatarSize.THIRTY_SIX}
title={member.title}
/>
<p className="GroupStorySettingsModal__members_item__name">
{member.title}
</p>
</div>
);
})}
<p className="GroupStorySettingsModal__members_help">
{i18n('icu:GroupStorySettingsModal__members_help', {
groupTitle: group.title,
})}
</p>
<hr className="StoriesSettingsModal__divider" />
<button
className="GroupStorySettingsModal__remove_group"
onClick={() => onRemoveGroup(group)}
type="button"
>
{i18n('icu:GroupStorySettingsModal__remove_group')}
</button>
</ModalPage>
);
};

View file

@ -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 (
<StoriesSettingsModal
candidateConversations={candidateConversations}
distributionLists={distributionLists}
groupStories={groupStories}
signalConnections={signalConnections}
hideStoriesSettings={hideStoriesSettings}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
me={me}
getConversationByUuid={getConversationByUuid}
onDeleteList={deleteDistributionList}
toggleGroupsForStorySend={toggleGroupsForStorySend}
onDistributionListCreated={createDistributionList}
onHideMyStoriesFrom={hideMyStoriesFrom}
onRemoveMember={removeMemberFromDistributionList}

View file

@ -10,6 +10,12 @@ import type { ConversationType } from '../state/ducks/conversations';
import type { UUIDStringType } from '../types/UUID';
import { isConversationUnregistered } from './isConversationUnregistered';
export type GroupMemberships = {
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: Array<GroupV2RequestingMembership>;
pendingMemberships: Array<GroupV2PendingMembership>;
};
export const getGroupMemberships = (
{
memberships = [],
@ -22,11 +28,7 @@ export const getGroupMemberships = (
>
>,
getConversationByUuid: (uuid: UUIDStringType) => undefined | ConversationType
): {
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: Array<GroupV2RequestingMembership>;
pendingMemberships: Array<GroupV2PendingMembership>;
} => ({
): GroupMemberships => ({
memberships: memberships.reduce(
(result: Array<GroupV2Membership>, membership) => {
const member = getConversationByUuid(membership.uuid);