Edit distribution lists via story settings menu
This commit is contained in:
parent
9986d10947
commit
e321e1fea8
42 changed files with 2403 additions and 102 deletions
|
@ -1123,6 +1123,12 @@
|
|||
"message": "Done",
|
||||
"description": "Label for done"
|
||||
},
|
||||
"update": {
|
||||
"message": "Update"
|
||||
},
|
||||
"next2": {
|
||||
"message": "Next"
|
||||
},
|
||||
"on": {
|
||||
"message": "On",
|
||||
"description": "Label for when something is turned on"
|
||||
|
@ -7123,6 +7129,36 @@
|
|||
"message": "Delete this story? It will also be deleted for everyone who received it.",
|
||||
"description": "Confirmation dialog description text for deleting a story"
|
||||
},
|
||||
"SignalConnectionsModal__title": {
|
||||
"message": "Signal Connections",
|
||||
"description": "The phrase/term: 'Signal Connections'"
|
||||
},
|
||||
"SignalConnectionsModal__header": {
|
||||
"message": "$connections$ are people you've chosen to trust, either by:",
|
||||
"description": "The beginning sentence to list the different ways a signal connection is formed",
|
||||
"placeholders": {
|
||||
"connections": {
|
||||
"content": "$1",
|
||||
"example": "Signal Connections"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SignalConnectionsModal__bullet--1": {
|
||||
"message": "Starting a conversation",
|
||||
"description": "A way that signal connection is formed"
|
||||
},
|
||||
"SignalConnectionsModal__bullet--2": {
|
||||
"message": "Accepting a message request",
|
||||
"description": "A way that signal connection is formed"
|
||||
},
|
||||
"SignalConnectionsModal__bullet--3": {
|
||||
"message": "Having them in your system contacts",
|
||||
"description": "A way that signal connection is formed"
|
||||
},
|
||||
"SignalConnectionsModal__footer": {
|
||||
"message": "Your connections can see your name and photo, and can see posts to \"My Story\" unless you hide it from them",
|
||||
"description": "Additional information about signal connections and the stories they can see"
|
||||
},
|
||||
"Stories__title": {
|
||||
"message": "Stories",
|
||||
"description": "Title for the stories list"
|
||||
|
@ -7169,6 +7205,154 @@
|
|||
"message": "Sending reaction...",
|
||||
"description": "Toast message"
|
||||
},
|
||||
"StoriesSettings__title": {
|
||||
"message": "Story settings",
|
||||
"description": "Title for the story settings modal"
|
||||
},
|
||||
"StoriesSettings__new-list": {
|
||||
"message": "New private story",
|
||||
"description": "Label to create a new private story list"
|
||||
},
|
||||
"StoriesSettings__viewers--singular": {
|
||||
"message": "$num$ viewer",
|
||||
"description": "A single viewer",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"content": "$1",
|
||||
"example": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoriesSettings__viewers--plural": {
|
||||
"message": "$num$ viewers",
|
||||
"description": "More than one viewer",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"content": "$1",
|
||||
"example": "14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoriesSettings__who-can-see": {
|
||||
"message": "Who can see this story",
|
||||
"description": "Title for the who can see this story section"
|
||||
},
|
||||
"StoriesSettings__add-viewer": {
|
||||
"message": "Add viewer",
|
||||
"description": "Button label to add a viewer to a story"
|
||||
},
|
||||
"StoriesSettings__remove--action": {
|
||||
"message": "Remove",
|
||||
"description": "Button to remove a member from a private list"
|
||||
},
|
||||
"StoriesSettings__remove--title": {
|
||||
"message": "Remove $title$",
|
||||
"description": "Title of the confirmation dialog, has a person's name",
|
||||
"placeholders": {
|
||||
"title": {
|
||||
"content": "$1",
|
||||
"example": "Aahron Lee"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoriesSettings__remove--body": {
|
||||
"message": "This person will no longer see your story.",
|
||||
"description": "Body of the confirmation dialog to remove someone from a private distribution list"
|
||||
},
|
||||
"StoriesSettings__replies-reactions--title": {
|
||||
"message": "Replies & reactions",
|
||||
"description": "Title for the replies & reactions section"
|
||||
},
|
||||
"StoriesSettings__replies-reactions--label": {
|
||||
"message": "Allow replies & reactions",
|
||||
"description": "Checkbox label to allow or disallow replies to your stories"
|
||||
},
|
||||
"StoriesSettings__replies-reactions--description": {
|
||||
"message": "Let people who can view your story react and reply.",
|
||||
"description": "Description of checkbox to allow or disallow replies to your stories"
|
||||
},
|
||||
"StoriesSettings__delete-list": {
|
||||
"message": "Delete private story",
|
||||
"description": "Button label to delete a private distribution list"
|
||||
},
|
||||
"StoriesSettings__delete-list--confirm": {
|
||||
"message": "Delete private story?",
|
||||
"description": "Confirmation text to delete a private distribution list"
|
||||
},
|
||||
"StoriesSettings__choose-viewers": {
|
||||
"message": "Choose Viewers",
|
||||
"description": "Modal title when choosing to add a viewer to a private distribution list"
|
||||
},
|
||||
"StoriesSettings__name-story": {
|
||||
"message": "Name this story",
|
||||
"description": "Modal title when naming a private distribution list"
|
||||
},
|
||||
"StoriesSettings__name-placeholder": {
|
||||
"message": "Story name (required)",
|
||||
"description": "Placeholder for input field"
|
||||
},
|
||||
"StoriesSettings__hide-story": {
|
||||
"message": "Hide story from",
|
||||
"description": "Modal title when hiding people from my stories"
|
||||
},
|
||||
"StoriesSettings__mine__all--label": {
|
||||
"message": "All Signal connections",
|
||||
"description": "Input label to describe all signal connections"
|
||||
},
|
||||
"StoriesSettings__mine__all--description": {
|
||||
"message": "Share with all connections",
|
||||
"description": "Description of button StoriesSettings__mine__all--label"
|
||||
},
|
||||
"StoriesSettings__mine__exclude--label": {
|
||||
"message": "All Signal connections except...",
|
||||
"description": "Input label to create a block list"
|
||||
},
|
||||
"StoriesSettings__mine__exclude--description": {
|
||||
"message": "$num$ people excluded",
|
||||
"description": "Description of how many people are excluded in a list",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"content": "$1",
|
||||
"example": "0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoriesSettings__mine__only--label": {
|
||||
"message": "Only share with...",
|
||||
"description": "Input label to create an exclusive allow list"
|
||||
},
|
||||
"StoriesSettings__mine__only--description": {
|
||||
"message": "Only share with selected people",
|
||||
"description": "Description of button StoriesSettings__mine__only--label"
|
||||
},
|
||||
"StoriesSettings__mine__only--description--people": {
|
||||
"message": "$num$ people",
|
||||
"description": "Description of how many people are in the exclusive allow list",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"content": "$1",
|
||||
"example": "7"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoriesSettings__mine__disclaimer": {
|
||||
"message": "Choose who can view your story. Changes won't affect stories you've already sent. $learnMore$",
|
||||
"description": "Disclaimer on how changes to story settings work",
|
||||
"placeholders": {
|
||||
"learnMore": {
|
||||
"content": "$1",
|
||||
"example": "<a>Learn more</a>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoriesSettings__mine__disclaimer--learn-more": {
|
||||
"message": "Learn more.",
|
||||
"description": "Learn more link to learn about who can view your story"
|
||||
},
|
||||
"StoriesSettings__context-menu": {
|
||||
"message": "Story settings",
|
||||
"description": "Button label to get to story settings"
|
||||
},
|
||||
"Stories__settings-toggle--title": {
|
||||
"message": "Share & View Stories",
|
||||
"description": "Select box title for the stories on/off toggle"
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"@babel/preset-typescript": "7.17.12",
|
||||
"@electron/fuses": "1.5.0",
|
||||
"@mixer/parallel-prettier": "2.0.1",
|
||||
"@signalapp/mock-server": "2.0.1",
|
||||
"@signalapp/mock-server": "2.1.0",
|
||||
"@storybook/addon-a11y": "6.5.6",
|
||||
"@storybook/addon-actions": "6.5.6",
|
||||
"@storybook/addon-controls": "6.5.6",
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
margin: 0;
|
||||
padding: 6px 0;
|
||||
width: auto;
|
||||
|
||||
&--single-item {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
|
@ -121,9 +125,15 @@
|
|||
&--focused,
|
||||
&:focus,
|
||||
&:active {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 1px 1px $color-ultramarine;
|
||||
outline: none;
|
||||
@include keyboard-mode {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 1px 1px $color-ultramarine;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__popper--single-item &__option {
|
||||
padding: 12px 6px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
max-height: 89vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include light-theme() {
|
||||
background: $color-white;
|
||||
color: $color-gray-90;
|
||||
|
@ -24,21 +23,76 @@
|
|||
}
|
||||
|
||||
&__header {
|
||||
position: sticky;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1em;
|
||||
padding: 16px 16px 0 16px;
|
||||
position: sticky;
|
||||
|
||||
&--with-back-button .module-Modal__title {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include font-body-1-bold;
|
||||
margin: 0 0 1em 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
@include button-reset;
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/chevron-left-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/chevron-left-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $color-gray-02;
|
||||
}
|
||||
&:active {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
}
|
||||
@include dark-theme {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $color-gray-80;
|
||||
}
|
||||
&:active {
|
||||
background: $color-gray-75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
@include button-reset;
|
||||
|
||||
border-radius: 4px;
|
||||
float: right;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
|
|
24
stylesheets/components/SignalConnectionsModal.scss
Normal file
24
stylesheets/components/SignalConnectionsModal.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.SignalConnectionsModal {
|
||||
color: $color-gray-05;
|
||||
|
||||
&__list {
|
||||
margin: 16px 0;
|
||||
|
||||
li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
|
||||
button {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,20 @@
|
|||
width: 380px;
|
||||
padding-top: calc(14px + var(--title-bar-drag-area-height));
|
||||
|
||||
&__settings {
|
||||
margin-left: 24px;
|
||||
opacity: 1;
|
||||
|
||||
&::after {
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/more-horiz-24.svg',
|
||||
$color-white
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
|
177
stylesheets/components/StoriesSettingsModal.scss
Normal file
177
stylesheets/components/StoriesSettingsModal.scss
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.StoriesSettingsModal {
|
||||
&__modal {
|
||||
.module-conversation-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.module-conversation-list__item--contact-or-conversation {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
@include button-reset;
|
||||
@include font-body-1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 52px;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&--no-pointer {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
&__viewers {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
@mixin avatar($svg) {
|
||||
@include rounded-corners;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: $color-gray-75;
|
||||
|
||||
&::after {
|
||||
@include color-svg($svg, $color-white);
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&--new {
|
||||
@include avatar('../images/icons/v2/plus-20.svg');
|
||||
}
|
||||
|
||||
&--private {
|
||||
@include avatar('../images/icons/v2/group-solid-24.svg');
|
||||
|
||||
&--large {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&__delete {
|
||||
@include button-reset;
|
||||
@include color-svg(
|
||||
'../images/icons/v2/trash-outline-24.svg',
|
||||
$color-gray-25
|
||||
);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover &__delete {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
border-color: $color-gray-65;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include font-body-1-bold;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__delete-list {
|
||||
@include button-reset;
|
||||
align-items: center;
|
||||
color: $color-accent-red;
|
||||
display: flex;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
|
||||
&::before {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/trash-outline-24.svg',
|
||||
$color-accent-red
|
||||
);
|
||||
content: '';
|
||||
height: 20px;
|
||||
margin-right: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
&__conversation-list {
|
||||
flex-grow: 1;
|
||||
min-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__search {
|
||||
&__container {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__tags {
|
||||
margin: 0 -4px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
align-items: center;
|
||||
background: $color-gray-75;
|
||||
border-radius: 26px;
|
||||
color: $color-gray-05;
|
||||
display: inline-flex;
|
||||
padding: 4px 0;
|
||||
margin: 0 4px;
|
||||
|
||||
&__name {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
@include button-reset;
|
||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-15);
|
||||
height: 12px;
|
||||
margin: 0 8px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__name-story-avatar-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__disclaimer {
|
||||
@include font-subtitle;
|
||||
color: $color-gray-25;
|
||||
|
||||
&__learn-more {
|
||||
@include button-reset;
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -103,6 +103,7 @@
|
|||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||
@import './components/Select.scss';
|
||||
@import './components/SignalConnectionsModal.scss';
|
||||
@import './components/Slider.scss';
|
||||
@import './components/StagedLinkPreview.scss';
|
||||
@import './components/Stories.scss';
|
||||
|
@ -110,6 +111,7 @@
|
|||
@import './components/StoryImage.scss';
|
||||
@import './components/StoryListItem.scss';
|
||||
@import './components/StoryReplyQuote.scss';
|
||||
@import './components/StoriesSettingsModal.scss';
|
||||
@import './components/StoryViewsNRepliesModal.scss';
|
||||
@import './components/StoryViewer.scss';
|
||||
@import './components/SystemMessage.scss';
|
||||
|
|
19
ts/Crypto.ts
19
ts/Crypto.ts
|
@ -1,9 +1,8 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
import pProps from 'p-props';
|
||||
import { chunk } from 'lodash';
|
||||
import Long from 'long';
|
||||
import { HKDF } from '@signalapp/libsignal-client';
|
||||
|
||||
|
@ -15,6 +14,8 @@ import { ProfileDecryptError } from './types/errors';
|
|||
import { UUID, UUID_BYTE_SIZE } from './types/UUID';
|
||||
import type { UUIDStringType } from './types/UUID';
|
||||
|
||||
export { uuidToBytes } from './util/uuidToBytes';
|
||||
|
||||
export { HashType, CipherType };
|
||||
|
||||
const PROFILE_IV_LENGTH = 12; // bytes
|
||||
|
@ -448,20 +449,6 @@ export async function encryptCdsDiscoveryRequest(
|
|||
};
|
||||
}
|
||||
|
||||
export function uuidToBytes(uuid: string): Uint8Array {
|
||||
if (uuid.length !== 36) {
|
||||
log.warn(
|
||||
'uuidToBytes: received a string of invalid length. ' +
|
||||
'Returning an empty Uint8Array'
|
||||
);
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
return Uint8Array.from(
|
||||
chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16))
|
||||
);
|
||||
}
|
||||
|
||||
export function bytesToUuid(bytes: Uint8Array): undefined | UUIDStringType {
|
||||
if (bytes.byteLength !== UUID_BYTE_SIZE) {
|
||||
log.warn(
|
||||
|
|
|
@ -15,6 +15,7 @@ export type PropsType = {
|
|||
moduleClassName?: string;
|
||||
name: string;
|
||||
onChange: (value: boolean) => unknown;
|
||||
onClick?: () => unknown;
|
||||
};
|
||||
|
||||
export const Checkbox = ({
|
||||
|
@ -26,6 +27,7 @@ export const Checkbox = ({
|
|||
moduleClassName,
|
||||
name,
|
||||
onChange,
|
||||
onClick,
|
||||
}: PropsType): JSX.Element => {
|
||||
const getClassName = getClassNamesFor('Checkbox', moduleClassName);
|
||||
const id = useMemo(() => `${name}::${uuid()}`, [name]);
|
||||
|
@ -39,12 +41,15 @@ export const Checkbox = ({
|
|||
id={id}
|
||||
name={name}
|
||||
onChange={ev => onChange(ev.target.checked)}
|
||||
onClick={onClick}
|
||||
type={isRadio ? 'radio' : 'checkbox'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<div className={getClassName('__description')}>{description}</div>
|
||||
<label htmlFor={id}>
|
||||
<div>{label}</div>
|
||||
<div className={getClassName('__description')}>{description}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -93,7 +93,9 @@ export function ContextMenuPopper<T>({
|
|||
>
|
||||
<div className={theme ? themeClassName(theme) : undefined}>
|
||||
<div
|
||||
className="ContextMenu__popper"
|
||||
className={classNames('ContextMenu__popper', {
|
||||
'ContextMenu__popper--single-item': menuOptions.length === 1,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
|
|
|
@ -34,9 +34,12 @@ import type { BodyRangeType, LocalizerType, ThemeType } from '../types/Util';
|
|||
import { ModalHost } from './ModalHost';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import { assert } from '../util/assert';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
import { useAnimated } from '../hooks/useAnimated';
|
||||
import {
|
||||
shouldNeverBeCalled,
|
||||
asyncShouldNeverBeCalled,
|
||||
} from '../util/shouldNeverBeCalled';
|
||||
|
||||
export type DataPropsType = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
|
@ -470,15 +473,3 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function shouldNeverBeCalled(..._args: ReadonlyArray<unknown>): void {
|
||||
assert(false, 'This should never be called. Doing nothing');
|
||||
}
|
||||
|
||||
async function asyncShouldNeverBeCalled(
|
||||
..._args: ReadonlyArray<unknown>
|
||||
): Promise<undefined> {
|
||||
shouldNeverBeCalled();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { missingCaseError } from '../util/missingCaseError';
|
|||
|
||||
import { ButtonVariant } from './Button';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { SignalConnectionsModal } from './SignalConnectionsModal';
|
||||
import { WhatsNewModal } from './WhatsNewModal';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -28,6 +29,12 @@ type PropsType = {
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId?: string;
|
||||
renderSafetyNumber: () => JSX.Element;
|
||||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible: boolean;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
// StoriesSettings
|
||||
isStoriesSettingsVisible: boolean;
|
||||
renderStoriesSettings: () => JSX.Element;
|
||||
// UserNotFoundModal
|
||||
hideUserNotFoundModal: () => unknown;
|
||||
userNotFoundModalState?: UserNotFoundModalStateType;
|
||||
|
@ -50,6 +57,12 @@ export const GlobalModalContainer = ({
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId,
|
||||
renderSafetyNumber,
|
||||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible,
|
||||
toggleSignalConnectionsModal,
|
||||
// StoriesSettings
|
||||
isStoriesSettingsVisible,
|
||||
renderStoriesSettings,
|
||||
// UserNotFoundModal
|
||||
hideUserNotFoundModal,
|
||||
userNotFoundModalState,
|
||||
|
@ -105,5 +118,18 @@ export const GlobalModalContainer = ({
|
|||
return renderForwardMessageModal();
|
||||
}
|
||||
|
||||
if (isSignalConnectionsVisible) {
|
||||
return (
|
||||
<SignalConnectionsModal
|
||||
i18n={i18n}
|
||||
onClose={toggleSignalConnectionsModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isStoriesSettingsVisible) {
|
||||
return renderStoriesSettings();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -23,7 +23,9 @@ const LOREM_IPSUM =
|
|||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.';
|
||||
|
||||
export const BareBonesShort = (): JSX.Element => (
|
||||
<Modal i18n={i18n}>Hello world!</Modal>
|
||||
<Modal i18n={i18n} useFocusTrap={false}>
|
||||
Hello world!
|
||||
</Modal>
|
||||
);
|
||||
|
||||
BareBonesShort.story = {
|
||||
|
@ -31,7 +33,7 @@ BareBonesShort.story = {
|
|||
};
|
||||
|
||||
export const BareBonesLong = (): JSX.Element => (
|
||||
<Modal i18n={i18n}>
|
||||
<Modal i18n={i18n} useFocusTrap={false}>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
|
@ -96,7 +98,7 @@ LotsOfButtonsInTheFooter.story = {
|
|||
};
|
||||
|
||||
export const LongBodyWithTitle = (): JSX.Element => (
|
||||
<Modal i18n={i18n} title="Hello world" onClose={onClose}>
|
||||
<Modal i18n={i18n} title="Hello world" onClose={onClose} useFocusTrap={false}>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
|
@ -195,3 +197,19 @@ export const StickyFooterLotsOfButtons = (): JSX.Element => (
|
|||
StickyFooterLotsOfButtons.story = {
|
||||
name: 'Sticky footer, Lots of buttons',
|
||||
};
|
||||
|
||||
export const WithBackButton = (): JSX.Element => (
|
||||
<Modal
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onBackButtonClick={noop}
|
||||
useFocusTrap={false}
|
||||
title="The Modal Title"
|
||||
>
|
||||
Hello world!
|
||||
</Modal>
|
||||
);
|
||||
|
||||
WithBackButton.story = {
|
||||
name: 'Back Button',
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ type PropsType = {
|
|||
hasXButton?: boolean;
|
||||
i18n: LocalizerType;
|
||||
moduleClassName?: string;
|
||||
onBackButtonClick?: () => unknown;
|
||||
onClose?: () => void;
|
||||
title?: ReactNode;
|
||||
useFocusTrap?: boolean;
|
||||
|
@ -42,6 +43,7 @@ export function Modal({
|
|||
i18n,
|
||||
moduleClassName,
|
||||
noMouseClose,
|
||||
onBackButtonClick,
|
||||
onClose = noop,
|
||||
title,
|
||||
theme,
|
||||
|
@ -70,6 +72,7 @@ export function Modal({
|
|||
hasXButton={hasXButton}
|
||||
i18n={i18n}
|
||||
moduleClassName={moduleClassName}
|
||||
onBackButtonClick={onBackButtonClick}
|
||||
onClose={close}
|
||||
title={title}
|
||||
>
|
||||
|
@ -86,6 +89,7 @@ export function ModalWindow({
|
|||
hasXButton,
|
||||
i18n,
|
||||
moduleClassName,
|
||||
onBackButtonClick,
|
||||
onClose = noop,
|
||||
title,
|
||||
}: Readonly<PropsType>): JSX.Element {
|
||||
|
@ -97,7 +101,7 @@ export function ModalWindow({
|
|||
const [scrolled, setScrolled] = useState(false);
|
||||
const [hasOverflow, setHasOverflow] = useState(false);
|
||||
|
||||
const hasHeader = Boolean(hasXButton || title);
|
||||
const hasHeader = Boolean(hasXButton || title || onBackButtonClick);
|
||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||
|
||||
function handleResize({ scroll }: ContentRect) {
|
||||
|
@ -127,14 +131,21 @@ export function ModalWindow({
|
|||
}}
|
||||
>
|
||||
{hasHeader && (
|
||||
<div className={getClassName('__header')}>
|
||||
{hasXButton && (
|
||||
<div
|
||||
className={classNames(
|
||||
getClassName('__header'),
|
||||
onBackButtonClick
|
||||
? getClassName('__header--with-back-button')
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{onBackButtonClick && (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
type="button"
|
||||
className={getClassName('__close-button')}
|
||||
aria-label={i18n('back')}
|
||||
className={getClassName('__back-button')}
|
||||
onClick={onBackButtonClick}
|
||||
tabIndex={0}
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
{title && (
|
||||
|
@ -147,6 +158,18 @@ export function ModalWindow({
|
|||
{title}
|
||||
</h1>
|
||||
)}
|
||||
{hasXButton && !title && (
|
||||
<div className={getClassName('__title')} />
|
||||
)}
|
||||
{hasXButton && (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className={getClassName('__close-button')}
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Measure scroll onResize={handleResize}>
|
||||
|
|
|
@ -7,8 +7,9 @@ import type { LocalizerType } from '../types/Util';
|
|||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { MY_STORIES_ID, StoryViewModeType } from '../types/Stories';
|
||||
import { StoryViewModeType } from '../types/Stories';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import { Theme } from '../util/theme';
|
||||
|
||||
|
@ -69,9 +70,11 @@ export const MyStories = ({
|
|||
{myStories.map(list => (
|
||||
<div className="MyStories__distribution" key={list.distributionId}>
|
||||
<div className="MyStories__distribution__title">
|
||||
{list.distributionId === MY_STORIES_ID
|
||||
? i18n('Stories__mine')
|
||||
: list.distributionName}
|
||||
<StoryDistributionListName
|
||||
i18n={i18n}
|
||||
id={list.distributionId}
|
||||
name={list.distributionName}
|
||||
/>
|
||||
</div>
|
||||
{list.stories.map(story => (
|
||||
<div className="MyStories__story" key={story.timestamp}>
|
||||
|
|
28
ts/components/SignalConnectionsModal.stories.tsx
Normal file
28
ts/components/SignalConnectionsModal.stories.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { PropsType } from './SignalConnectionsModal';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { SignalConnectionsModal } from './SignalConnectionsModal';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SignalConnectionsModal',
|
||||
component: SignalConnectionsModal,
|
||||
argTypes: {
|
||||
i18n: {
|
||||
defaultValue: i18n,
|
||||
},
|
||||
onClose: { action: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<PropsType> = args => <SignalConnectionsModal {...args} />;
|
||||
|
||||
export const Modal = Template.bind({});
|
||||
Modal.args = {};
|
55
ts/components/SignalConnectionsModal.tsx
Normal file
55
ts/components/SignalConnectionsModal.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Intl } from './Intl';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const SignalConnectionsModal = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return (
|
||||
<Modal hasXButton i18n={i18n} onClose={onClose}>
|
||||
<div className="SignalConnectionsModal">
|
||||
<i className="SignalConnectionsModal__icon" />
|
||||
|
||||
<div className="SignalConnectionsModal__description">
|
||||
<Intl
|
||||
components={{
|
||||
connections: (
|
||||
<strong>{i18n('SignalConnectionsModal__title')}</strong>
|
||||
),
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="SignalConnectionsModal__header"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className="SignalConnectionsModal__list">
|
||||
<li>{i18n('SignalConnectionsModal__bullet--1')}</li>
|
||||
<li>{i18n('SignalConnectionsModal__bullet--2')}</li>
|
||||
<li>{i18n('SignalConnectionsModal__bullet--3')}</li>
|
||||
</ul>
|
||||
|
||||
<div className="SignalConnectionsModal__description">
|
||||
{i18n('SignalConnectionsModal__footer')}
|
||||
</div>
|
||||
|
||||
<div className="SignalConnectionsModal__button">
|
||||
<Button onClick={onClose} variant={ButtonVariant.Primary}>
|
||||
{i18n('Confirmation--confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -46,6 +46,7 @@ export default {
|
|||
renderStoryCreator: { action: true },
|
||||
renderStoryViewer: { action: true },
|
||||
showConversation: { action: true },
|
||||
showStoriesSettings: { action: true },
|
||||
stories: {
|
||||
defaultValue: [],
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ export type PropsType = {
|
|||
queueStoryDownload: (storyId: string) => unknown;
|
||||
renderStoryCreator: (props: SmartStoryCreatorPropsType) => JSX.Element;
|
||||
showConversation: ShowConversationType;
|
||||
showStoriesSettings: () => unknown;
|
||||
stories: Array<ConversationStoryType>;
|
||||
toggleHideStories: (conversationId: string) => unknown;
|
||||
toggleStoriesView: () => unknown;
|
||||
|
@ -52,6 +53,7 @@ export const Stories = ({
|
|||
queueStoryDownload,
|
||||
renderStoryCreator,
|
||||
showConversation,
|
||||
showStoriesSettings,
|
||||
stories,
|
||||
toggleHideStories,
|
||||
toggleStoriesView,
|
||||
|
@ -98,6 +100,7 @@ export const Stories = ({
|
|||
setIsShowingStoryCreator(true);
|
||||
}
|
||||
}}
|
||||
onStoriesSettings={showStoriesSettings}
|
||||
onStoryClicked={viewUserStories}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
showConversation={showConversation}
|
||||
|
|
|
@ -15,9 +15,11 @@ import type {
|
|||
StoryViewType,
|
||||
} from '../types/Stories';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { MyStoriesButton } from './MyStoriesButton';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StoryListItem } from './StoryListItem';
|
||||
import { Theme } from '../util/theme';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
|
||||
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationStoryType> = {
|
||||
|
@ -63,6 +65,7 @@ export type PropsType = {
|
|||
myStories: Array<MyStoryType>;
|
||||
onAddStory: () => unknown;
|
||||
onMyStoriesClicked: () => unknown;
|
||||
onStoriesSettings: () => unknown;
|
||||
onStoryClicked: (conversationId: string) => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
showConversation: ShowConversationType;
|
||||
|
@ -78,6 +81,7 @@ export const StoriesPane = ({
|
|||
myStories,
|
||||
onAddStory,
|
||||
onMyStoriesClicked,
|
||||
onStoriesSettings,
|
||||
onStoryClicked,
|
||||
queueStoryDownload,
|
||||
showConversation,
|
||||
|
@ -117,6 +121,21 @@ export const StoriesPane = ({
|
|||
onClick={onAddStory}
|
||||
type="button"
|
||||
/>
|
||||
<ContextMenu
|
||||
buttonClassName="Stories__pane__settings"
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
{
|
||||
onClick: () => onStoriesSettings(),
|
||||
label: i18n('StoriesSettings__context-menu'),
|
||||
},
|
||||
]}
|
||||
popperOptions={{
|
||||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
/>
|
||||
</div>
|
||||
<SearchInput
|
||||
i18n={i18n}
|
||||
|
|
105
ts/components/StoriesSettingsModal.stories.tsx
Normal file
105
ts/components/StoriesSettingsModal.stories.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { PropsType } from './StoriesSettingsModal';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { MY_STORIES_ID } from '../types/Stories';
|
||||
import { StoriesSettingsModal } from './StoriesSettingsModal';
|
||||
import { UUID } from '../types/UUID';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/StoriesSettingsModal',
|
||||
component: StoriesSettingsModal,
|
||||
argTypes: {
|
||||
candidateConversations: {
|
||||
defaultValue: Array.from(Array(100), () => getDefaultConversation()),
|
||||
},
|
||||
distributionLists: {
|
||||
defaultValue: [],
|
||||
},
|
||||
getPreferredBadge: { action: true },
|
||||
hideStoriesSettings: { action: true },
|
||||
i18n: {
|
||||
defaultValue: i18n,
|
||||
},
|
||||
me: {
|
||||
defaultValue: getDefaultConversation(),
|
||||
},
|
||||
onDeleteList: { action: true },
|
||||
onDistributionListCreated: { action: true },
|
||||
onHideMyStoriesFrom: { action: true },
|
||||
onRemoveMember: { action: true },
|
||||
onRepliesNReactionsChanged: { action: true },
|
||||
onViewersUpdated: { action: true },
|
||||
setMyStoriesToAllSignalConnections: { action: true },
|
||||
toggleSignalConnectionsModal: { action: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<PropsType> = args => <StoriesSettingsModal {...args} />;
|
||||
|
||||
export const MyStories = Template.bind({});
|
||||
MyStories.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: false,
|
||||
members: [],
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const MyStoriesBlockList = Template.bind({});
|
||||
MyStoriesBlockList.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: true,
|
||||
members: Array.from(Array(2), () => getDefaultConversation()),
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const MyStoriesExclusive = Template.bind({});
|
||||
MyStoriesExclusive.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: false,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: false,
|
||||
members: Array.from(Array(11), () => getDefaultConversation()),
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const SingleList = Template.bind({});
|
||||
SingleList.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: false,
|
||||
members: [],
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: UUID.generate().toString(),
|
||||
isBlockList: false,
|
||||
members: Array.from(Array(4), () => getDefaultConversation()),
|
||||
name: 'Thailand 2021',
|
||||
},
|
||||
],
|
||||
};
|
766
ts/components/StoriesSettingsModal.tsx
Normal file
766
ts/components/StoriesSettingsModal.tsx
Normal file
|
@ -0,0 +1,766 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { MeasuredComponentProps } from 'react-measure';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { Row } from './ConversationList';
|
||||
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 { ConversationList, RowType } from './ConversationList';
|
||||
import { Input } from './Input';
|
||||
import { Intl } from './Intl';
|
||||
import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
|
||||
import { Modal } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { Theme } from '../util/theme';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { UUID } from '../types/UUID';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import {
|
||||
shouldNeverBeCalled,
|
||||
asyncShouldNeverBeCalled,
|
||||
} from '../util/shouldNeverBeCalled';
|
||||
|
||||
export type PropsType = {
|
||||
candidateConversations: Array<ConversationType>;
|
||||
distributionLists: Array<StoryDistributionListWithMembersDataType>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
hideStoriesSettings: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
me: ConversationType;
|
||||
onDeleteList: (listId: string) => unknown;
|
||||
onDistributionListCreated: (
|
||||
name: string,
|
||||
viewerUuids: Array<UUIDStringType>
|
||||
) => unknown;
|
||||
onHideMyStoriesFrom: (viewerUuids: Array<UUIDStringType>) => unknown;
|
||||
onRemoveMember: (listId: string, uuid: UUIDStringType | undefined) => unknown;
|
||||
onRepliesNReactionsChanged: (
|
||||
listId: string,
|
||||
allowsReplies: boolean
|
||||
) => unknown;
|
||||
onViewersUpdated: (
|
||||
listId: string,
|
||||
viewerUuids: Array<UUIDStringType>
|
||||
) => unknown;
|
||||
setMyStoriesToAllSignalConnections: () => unknown;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
};
|
||||
|
||||
enum Page {
|
||||
DistributionLists = 'DistributionLists',
|
||||
AddViewer = 'AddViewer',
|
||||
ChooseViewers = 'ChooseViewers',
|
||||
NameStory = 'NameStory',
|
||||
HideStoryFrom = 'HideStoryFrom',
|
||||
}
|
||||
|
||||
export const StoriesSettingsModal = ({
|
||||
candidateConversations,
|
||||
distributionLists,
|
||||
getPreferredBadge,
|
||||
hideStoriesSettings,
|
||||
i18n,
|
||||
me,
|
||||
onDeleteList,
|
||||
onDistributionListCreated,
|
||||
onHideMyStoriesFrom,
|
||||
onRemoveMember,
|
||||
onRepliesNReactionsChanged,
|
||||
onViewersUpdated,
|
||||
setMyStoriesToAllSignalConnections,
|
||||
toggleSignalConnectionsModal,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [listToEditId, setListToEditId] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const listToEdit = useMemo(
|
||||
() => distributionLists.find(x => x.id === listToEditId),
|
||||
[distributionLists, listToEditId]
|
||||
);
|
||||
|
||||
const [page, setPage] = useState<Page>(Page.DistributionLists);
|
||||
|
||||
const [storyName, setStoryName] = useState('');
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const [filteredConversations, setFilteredConversations] = useState(
|
||||
filterAndSortConversationsByRecent(
|
||||
candidateConversations,
|
||||
searchTerm,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
|
||||
const [selectedContacts, setSelectedContacts] = useState<
|
||||
Array<ConversationType>
|
||||
>([]);
|
||||
|
||||
const contactLookup = useMemo(() => {
|
||||
const map = new Map();
|
||||
candidateConversations.forEach(contact => {
|
||||
map.set(contact.id, contact);
|
||||
});
|
||||
return map;
|
||||
}, [candidateConversations]);
|
||||
|
||||
const toggleSelectedConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
let removeContact = false;
|
||||
const nextSelectedContacts = selectedContacts.filter(contact => {
|
||||
if (contact.id === conversationId) {
|
||||
removeContact = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (removeContact) {
|
||||
setSelectedContacts(nextSelectedContacts);
|
||||
return;
|
||||
}
|
||||
const selectedContact = contactLookup.get(conversationId);
|
||||
if (selectedContact) {
|
||||
setSelectedContacts([...nextSelectedContacts, selectedContact]);
|
||||
}
|
||||
},
|
||||
[contactLookup, selectedContacts, setSelectedContacts]
|
||||
);
|
||||
|
||||
const normalizedSearchTerm = searchTerm.trim();
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setFilteredConversations(
|
||||
filterAndSortConversationsByRecent(
|
||||
candidateConversations,
|
||||
normalizedSearchTerm,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
}, 200);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [candidateConversations, normalizedSearchTerm, setFilteredConversations]);
|
||||
|
||||
const resetChooseViewersScreen = useCallback(() => {
|
||||
setSelectedContacts([]);
|
||||
setSearchTerm('');
|
||||
setPage(Page.DistributionLists);
|
||||
}, []);
|
||||
|
||||
const selectedConversationUuids: Set<UUIDStringType> = useMemo(
|
||||
() =>
|
||||
new Set(selectedContacts.map(contact => contact.uuid).filter(isNotNil)),
|
||||
[selectedContacts]
|
||||
);
|
||||
|
||||
const [confirmDeleteListId, setConfirmDeleteListId] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [confirmRemoveMember, setConfirmRemoveMember] = useState<
|
||||
| undefined
|
||||
| {
|
||||
listId: string;
|
||||
title: string;
|
||||
uuid: UUIDStringType | undefined;
|
||||
}
|
||||
>();
|
||||
|
||||
let content: JSX.Element;
|
||||
if (page === Page.NameStory) {
|
||||
content = (
|
||||
<>
|
||||
<div className="StoriesSettingsModal__name-story-avatar-container">
|
||||
<div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" />
|
||||
</div>
|
||||
|
||||
<Input
|
||||
i18n={i18n}
|
||||
onChange={setStoryName}
|
||||
placeholder={i18n('StoriesSettings__name-placeholder')}
|
||||
value={storyName}
|
||||
/>
|
||||
|
||||
<div className="StoriesSettingsModal__title">
|
||||
{i18n('StoriesSettings__who-can-see')}
|
||||
</div>
|
||||
|
||||
{selectedContacts.map(contact => (
|
||||
<div
|
||||
className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"
|
||||
key={contact.id}
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType={contact.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={ThemeType.dark}
|
||||
title={contact.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{contact.title}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else if (
|
||||
page === Page.AddViewer ||
|
||||
page === Page.ChooseViewers ||
|
||||
page === Page.HideStoryFrom
|
||||
) {
|
||||
const rowCount = filteredConversations.length;
|
||||
const getRow = (index: number): undefined | Row => {
|
||||
const contact = filteredConversations[index];
|
||||
if (!contact || !contact.uuid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isSelected = selectedConversationUuids.has(
|
||||
UUID.fromString(contact.uuid)
|
||||
);
|
||||
|
||||
return {
|
||||
type: RowType.ContactCheckbox,
|
||||
contact,
|
||||
isChecked: isSelected,
|
||||
};
|
||||
};
|
||||
|
||||
content = (
|
||||
<>
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
i18n={i18n}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
moduleClassName="StoriesSettingsModal__search"
|
||||
onChange={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{selectedContacts.length ? (
|
||||
<div className="StoriesSettingsModal__tags">
|
||||
{selectedContacts.map(contact => (
|
||||
<div className="StoriesSettingsModal__tag" key={contact.id}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType={contact.type}
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
theme={ThemeType.dark}
|
||||
title={contact.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__tag__name">
|
||||
{contact.firstName ||
|
||||
contact.profileName ||
|
||||
contact.phoneNumber}
|
||||
</span>
|
||||
<button
|
||||
aria-label={i18n('StoriesSettings__remove--title', [
|
||||
contact.title,
|
||||
])}
|
||||
className="StoriesSettingsModal__tag__remove"
|
||||
onClick={() => toggleSelectedConversation(contact.id)}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : undefined}
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }: MeasuredComponentProps) => (
|
||||
<div
|
||||
className="StoriesSettingsModal__conversation-list"
|
||||
ref={measureRef}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={contentRect.bounds}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(conversationId: string) => {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}}
|
||||
lookupConversationWithoutUuid={asyncShouldNeverBeCalled}
|
||||
showConversation={shouldNeverBeCalled}
|
||||
showUserNotFoundModal={shouldNeverBeCalled}
|
||||
setIsFetchingUUID={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
return <div />;
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
theme={ThemeType.dark}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
||||
{i18n('noContactsFound')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (listToEdit) {
|
||||
const isMyStories = listToEdit.id === MY_STORIES_ID;
|
||||
|
||||
content = (
|
||||
<>
|
||||
{!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 && (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={!listToEdit.members.length}
|
||||
description={i18n('StoriesSettings__mine__all--description')}
|
||||
isRadio
|
||||
label={i18n('StoriesSettings__mine__all--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="share"
|
||||
onChange={() => {
|
||||
setMyStoriesToAllSignalConnections();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={listToEdit.isBlockList && listToEdit.members.length > 0}
|
||||
description={i18n('StoriesSettings__mine__exclude--description', [
|
||||
listToEdit.isBlockList
|
||||
? String(listToEdit.members.length)
|
||||
: '0',
|
||||
])}
|
||||
isRadio
|
||||
label={i18n('StoriesSettings__mine__exclude--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="share"
|
||||
onChange={noop}
|
||||
onClick={() => {
|
||||
if (listToEdit.isBlockList) {
|
||||
setSelectedContacts(listToEdit.members);
|
||||
}
|
||||
setPage(Page.HideStoryFrom);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={!listToEdit.isBlockList && listToEdit.members.length > 0}
|
||||
description={
|
||||
!listToEdit.isBlockList && listToEdit.members.length
|
||||
? i18n('StoriesSettings__mine__only--description--people', [
|
||||
String(listToEdit.members.length),
|
||||
])
|
||||
: i18n('StoriesSettings__mine__only--description')
|
||||
}
|
||||
isRadio
|
||||
label={i18n('StoriesSettings__mine__only--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="share"
|
||||
onChange={noop}
|
||||
onClick={() => {
|
||||
if (!listToEdit.isBlockList) {
|
||||
setSelectedContacts(listToEdit.members);
|
||||
}
|
||||
setPage(Page.AddViewer);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="StoriesSettingsModal__disclaimer">
|
||||
<Intl
|
||||
components={{
|
||||
learnMore: (
|
||||
<button
|
||||
className="StoriesSettingsModal__disclaimer__learn-more"
|
||||
onClick={toggleSignalConnectionsModal}
|
||||
type="button"
|
||||
>
|
||||
{i18n('StoriesSettings__mine__disclaimer--learn-more')}
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="StoriesSettings__mine__disclaimer"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!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 {
|
||||
const privateStories = distributionLists.filter(
|
||||
list => list.id !== MY_STORIES_ID
|
||||
);
|
||||
|
||||
content = (
|
||||
<>
|
||||
<button
|
||||
className="StoriesSettingsModal__list"
|
||||
onClick={() => {
|
||||
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" />
|
||||
</button>
|
||||
|
||||
<hr className="StoriesSettingsModal__divider" />
|
||||
|
||||
<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>
|
||||
{privateStories.map(list => (
|
||||
<button
|
||||
className="StoriesSettingsModal__list"
|
||||
key={list.id}
|
||||
onClick={() => {
|
||||
setListToEditId(list.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<span className="StoriesSettingsModal__list__avatar--private" />
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{list.name}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="StoriesSettingsModal__list__viewers">
|
||||
{list.members.length === 1
|
||||
? i18n('StoriesSettingsModal__list__viewers--singular', ['1'])
|
||||
: i18n('StoriesSettings__viewers--plural', [
|
||||
String(list.members.length),
|
||||
])}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const isChoosingViewers =
|
||||
page === Page.ChooseViewers || page === Page.AddViewer;
|
||||
|
||||
let modalTitle: string = i18n('StoriesSettings__title');
|
||||
if (page === Page.HideStoryFrom) {
|
||||
modalTitle = i18n('StoriesSettings__hide-story');
|
||||
} else if (page === Page.NameStory) {
|
||||
modalTitle = i18n('StoriesSettings__name-story');
|
||||
} else if (isChoosingViewers) {
|
||||
modalTitle = i18n('StoriesSettings__choose-viewers');
|
||||
} else if (listToEdit) {
|
||||
modalTitle = getStoryDistributionListName(
|
||||
i18n,
|
||||
listToEdit.id,
|
||||
listToEdit.name
|
||||
);
|
||||
}
|
||||
|
||||
const hasBackButton = page !== Page.DistributionLists || listToEdit;
|
||||
const hasStickyButtons =
|
||||
isChoosingViewers || page === Page.NameStory || page === Page.HideStoryFrom;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
hasStickyButtons={hasStickyButtons}
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
moduleClassName="StoriesSettingsModal__modal"
|
||||
onBackButtonClick={
|
||||
hasBackButton
|
||||
? () => {
|
||||
if (page === Page.HideStoryFrom) {
|
||||
resetChooseViewersScreen();
|
||||
} else if (page === Page.NameStory) {
|
||||
setPage(Page.ChooseViewers);
|
||||
} else if (isChoosingViewers) {
|
||||
resetChooseViewersScreen();
|
||||
} else if (listToEdit) {
|
||||
setListToEditId(undefined);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClose={hideStoriesSettings}
|
||||
theme={Theme.Dark}
|
||||
title={modalTitle}
|
||||
>
|
||||
{content}
|
||||
{isChoosingViewers && (
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
disabled={selectedContacts.length === 0}
|
||||
onClick={() => {
|
||||
if (listToEdit && page === Page.AddViewer) {
|
||||
onViewersUpdated(
|
||||
listToEdit.id,
|
||||
Array.from(selectedConversationUuids)
|
||||
);
|
||||
resetChooseViewersScreen();
|
||||
}
|
||||
|
||||
if (page === Page.ChooseViewers) {
|
||||
setPage(Page.NameStory);
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{page === Page.AddViewer ? i18n('done') : i18n('next2')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
)}
|
||||
{page === Page.NameStory && (
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
disabled={!storyName}
|
||||
onClick={() => {
|
||||
onDistributionListCreated(
|
||||
storyName,
|
||||
Array.from(selectedConversationUuids)
|
||||
);
|
||||
setStoryName('');
|
||||
resetChooseViewersScreen();
|
||||
}}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{i18n('done')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
)}
|
||||
{page === Page.HideStoryFrom && (
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
disabled={selectedContacts.length === 0}
|
||||
onClick={() => {
|
||||
onHideMyStoriesFrom(Array.from(selectedConversationUuids));
|
||||
resetChooseViewersScreen();
|
||||
}}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{i18n('update')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
)}
|
||||
</Modal>
|
||||
{confirmDeleteListId && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => {
|
||||
onDeleteList(confirmDeleteListId);
|
||||
setListToEditId(undefined);
|
||||
},
|
||||
style: 'negative',
|
||||
text: i18n('delete'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setConfirmDeleteListId(undefined);
|
||||
}}
|
||||
>
|
||||
{i18n('StoriesSettings__delete-list--confirm')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
{confirmRemoveMember && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () =>
|
||||
onRemoveMember(
|
||||
confirmRemoveMember.listId,
|
||||
confirmRemoveMember.uuid
|
||||
),
|
||||
style: 'negative',
|
||||
text: i18n('StoriesSettings__remove--action'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setConfirmRemoveMember(undefined);
|
||||
}}
|
||||
title={i18n('StoriesSettings__remove--title', [
|
||||
confirmRemoveMember.title,
|
||||
])}
|
||||
>
|
||||
{i18n('StoriesSettings__remove--body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
21
ts/components/StoryDistributionListName.tsx
Normal file
21
ts/components/StoryDistributionListName.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { getStoryDistributionListName } from '../types/Stories';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const StoryDistributionListName = ({
|
||||
i18n,
|
||||
id,
|
||||
name,
|
||||
}: PropsType): JSX.Element => {
|
||||
return <>{getStoryDistributionListName(i18n, id, name)}</>;
|
||||
};
|
|
@ -15,7 +15,6 @@ import Measure from 'react-measure';
|
|||
|
||||
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
||||
import { getUsernameFromSearch } from '../../../../types/Username';
|
||||
import { assert } from '../../../../util/assert';
|
||||
import { refMerger } from '../../../../util/refMerger';
|
||||
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
||||
import { missingCaseError } from '../../../../util/missingCaseError';
|
||||
|
@ -40,6 +39,7 @@ import { ConversationList, RowType } from '../../../ConversationList';
|
|||
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox';
|
||||
import { Button, ButtonVariant } from '../../../Button';
|
||||
import { SearchInput } from '../../../SearchInput';
|
||||
import { shouldNeverBeCalled } from '../../../../util/shouldNeverBeCalled';
|
||||
|
||||
export type StatePropsType = {
|
||||
regionCode: string | undefined;
|
||||
|
@ -399,7 +399,3 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
</ModalHost>
|
||||
);
|
||||
};
|
||||
|
||||
function shouldNeverBeCalled(..._args: ReadonlyArray<unknown>): unknown {
|
||||
assert(false, 'This should never be called. Doing nothing');
|
||||
}
|
||||
|
|
|
@ -15,12 +15,16 @@ export async function loadDistributionLists(): Promise<void> {
|
|||
export function getDistributionListsForRedux(): Array<StoryDistributionListDataType> {
|
||||
strictAssert(distributionLists, 'distributionLists has not been loaded');
|
||||
|
||||
const lists = distributionLists.map(list => ({
|
||||
allowsReplies: Boolean(list.allowsReplies),
|
||||
id: list.id,
|
||||
isBlockList: Boolean(list.isBlockList),
|
||||
name: list.name,
|
||||
}));
|
||||
const lists = distributionLists
|
||||
.map(list => ({
|
||||
allowsReplies: Boolean(list.allowsReplies),
|
||||
deletedAtTimestamp: list.deletedAtTimestamp,
|
||||
id: list.id,
|
||||
isBlockList: Boolean(list.isBlockList),
|
||||
name: list.name,
|
||||
memberUuids: list.members,
|
||||
}))
|
||||
.filter(list => !list.deletedAtTimestamp);
|
||||
|
||||
distributionLists = undefined;
|
||||
|
||||
|
|
|
@ -52,6 +52,8 @@ import type {
|
|||
UnknownRecord,
|
||||
} from '../types/StorageService.d';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
import type { StoryDistributionWithMembersType } from '../sql/Interface';
|
||||
import { MY_STORIES_ID } from '../types/Stories';
|
||||
|
||||
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
|
||||
|
||||
|
@ -328,12 +330,17 @@ async function generateManifest(
|
|||
);
|
||||
|
||||
storyDistributionLists.forEach(storyDistributionList => {
|
||||
const storageRecord = new Proto.StorageRecord();
|
||||
storageRecord.storyDistributionList = toStoryDistributionListRecord(
|
||||
storyDistributionList
|
||||
);
|
||||
|
||||
const { isNewItem, storageID } = processStorageRecord({
|
||||
currentStorageID: storyDistributionList.storageID,
|
||||
currentStorageVersion: storyDistributionList.storageVersion,
|
||||
identifierType: ITEM_TYPE.STORY_DISTRIBUTION_LIST,
|
||||
storageNeedsSync: storyDistributionList.storageNeedsSync,
|
||||
storageRecord: toStoryDistributionListRecord(storyDistributionList),
|
||||
storageRecord,
|
||||
});
|
||||
|
||||
if (isNewItem) {
|
||||
|
@ -1018,6 +1025,35 @@ async function processManifest(
|
|||
}
|
||||
});
|
||||
|
||||
// Check to make sure we have a "My Stories" distribution list set up
|
||||
const myStories = await dataInterface.getStoryDistributionWithMembers(
|
||||
MY_STORIES_ID
|
||||
);
|
||||
|
||||
if (!myStories) {
|
||||
const storyDistribution: StoryDistributionWithMembersType = {
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: true,
|
||||
members: [],
|
||||
name: MY_STORIES_ID,
|
||||
senderKeyInfo: undefined,
|
||||
storageNeedsSync: true,
|
||||
};
|
||||
|
||||
await dataInterface.createNewStoryDistribution(storyDistribution);
|
||||
|
||||
const shouldSave = false;
|
||||
window.reduxActions.storyDistributionLists.createDistributionList(
|
||||
storyDistribution.name,
|
||||
storyDistribution.members,
|
||||
storyDistribution,
|
||||
shouldSave
|
||||
);
|
||||
|
||||
conflictCount += 1;
|
||||
}
|
||||
|
||||
log.info(
|
||||
`storageService.process(${version}): conflictCount=${conflictCount}`
|
||||
);
|
||||
|
|
|
@ -1277,12 +1277,14 @@ export async function mergeStoryDistributionListRecord(
|
|||
|
||||
if (!localStoryDistributionList) {
|
||||
await dataInterface.createNewStoryDistribution(storyDistribution);
|
||||
window.reduxActions.storyDistributionLists.createDistributionList({
|
||||
allowsReplies: Boolean(storyDistribution.allowsReplies),
|
||||
id: storyDistribution.id,
|
||||
isBlockList: Boolean(storyDistribution.isBlockList),
|
||||
name: storyDistribution.name,
|
||||
});
|
||||
|
||||
const shouldSave = false;
|
||||
window.reduxActions.storyDistributionLists.createDistributionList(
|
||||
storyDistribution.name,
|
||||
remoteListMembers,
|
||||
storyDistribution,
|
||||
shouldSave
|
||||
);
|
||||
|
||||
return {
|
||||
details,
|
||||
|
@ -1306,8 +1308,6 @@ export async function mergeStoryDistributionListRecord(
|
|||
storyDistributionListRecord
|
||||
);
|
||||
|
||||
const needsUpdate = needsToClearUnknownFields || hasConflict;
|
||||
|
||||
const localMembersListSet = new Set(localStoryDistributionList.members);
|
||||
const toAdd: Array<UUIDStringType> = remoteListMembers.filter(
|
||||
uuid => !localMembersListSet.has(uuid)
|
||||
|
@ -1319,6 +1319,10 @@ export async function mergeStoryDistributionListRecord(
|
|||
uuid => !remoteMemberListSet.has(uuid)
|
||||
);
|
||||
|
||||
const needsUpdate = Boolean(
|
||||
needsToClearUnknownFields || hasConflict || toAdd.length || toRemove.length
|
||||
);
|
||||
|
||||
if (!needsUpdate) {
|
||||
return {
|
||||
details: [...details, ...conflictDetails],
|
||||
|
@ -1335,8 +1339,11 @@ export async function mergeStoryDistributionListRecord(
|
|||
});
|
||||
window.reduxActions.storyDistributionLists.modifyDistributionList({
|
||||
allowsReplies: Boolean(storyDistribution.allowsReplies),
|
||||
deletedAtTimestamp: storyDistribution.deletedAtTimestamp,
|
||||
id: storyDistribution.id,
|
||||
isBlockList: Boolean(storyDistribution.isBlockList),
|
||||
membersToAdd: toAdd,
|
||||
membersToRemove: toRemove,
|
||||
name: storyDistribution.name,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -240,8 +240,8 @@ export type StoryDistributionType = Readonly<{
|
|||
isBlockList: boolean;
|
||||
senderKeyInfo: SenderKeyInfoType | undefined;
|
||||
|
||||
storageID: string;
|
||||
storageVersion: number;
|
||||
storageID?: string;
|
||||
storageVersion?: number;
|
||||
storageUnknownFields?: Uint8Array | null;
|
||||
storageNeedsSync: boolean;
|
||||
}>;
|
||||
|
|
|
@ -4062,6 +4062,8 @@ type StoryDistributionForDatabase = Readonly<
|
|||
deletedAtTimestamp: number | null;
|
||||
isBlockList: 0 | 1;
|
||||
senderKeyInfoJson: string | null;
|
||||
storageID: string | null;
|
||||
storageVersion: number | null;
|
||||
storageNeedsSync: 0 | 1;
|
||||
} & Omit<
|
||||
StoryDistributionType,
|
||||
|
@ -4069,6 +4071,8 @@ type StoryDistributionForDatabase = Readonly<
|
|||
| 'deletedAtTimestamp'
|
||||
| 'isBlockList'
|
||||
| 'senderKeyInfo'
|
||||
| 'storageID'
|
||||
| 'storageVersion'
|
||||
| 'storageNeedsSync'
|
||||
>
|
||||
>;
|
||||
|
@ -4084,6 +4088,8 @@ function hydrateStoryDistribution(
|
|||
senderKeyInfo: fromDatabase.senderKeyInfoJson
|
||||
? JSON.parse(fromDatabase.senderKeyInfoJson)
|
||||
: undefined,
|
||||
storageID: fromDatabase.storageID || undefined,
|
||||
storageVersion: fromDatabase.storageVersion || undefined,
|
||||
storageNeedsSync: Boolean(fromDatabase.storageNeedsSync),
|
||||
storageUnknownFields: fromDatabase.storageUnknownFields || undefined,
|
||||
};
|
||||
|
@ -4099,6 +4105,8 @@ function freezeStoryDistribution(
|
|||
senderKeyInfoJson: story.senderKeyInfo
|
||||
? JSON.stringify(story.senderKeyInfo)
|
||||
: null,
|
||||
storageID: story.storageID || null,
|
||||
storageVersion: story.storageVersion || null,
|
||||
storageNeedsSync: story.storageNeedsSync ? 1 : 0,
|
||||
storageUnknownFields: story.storageUnknownFields || null,
|
||||
};
|
||||
|
|
|
@ -16,6 +16,8 @@ export type GlobalModalsStateType = {
|
|||
readonly contactModalState?: ContactModalStateType;
|
||||
readonly forwardMessageProps?: ForwardMessagePropsType;
|
||||
readonly isProfileEditorVisible: boolean;
|
||||
readonly isStoriesSettingsVisible: boolean;
|
||||
readonly isSignalConnectionsVisible: boolean;
|
||||
readonly isWhatsNewVisible: boolean;
|
||||
readonly profileEditorHasError: boolean;
|
||||
readonly safetyNumberModalContactId?: string;
|
||||
|
@ -30,12 +32,16 @@ const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
|
|||
const SHOW_WHATS_NEW_MODAL = 'globalModals/SHOW_WHATS_NEW_MODAL_MODAL';
|
||||
const HIDE_UUID_NOT_FOUND_MODAL = 'globalModals/HIDE_UUID_NOT_FOUND_MODAL';
|
||||
const SHOW_UUID_NOT_FOUND_MODAL = 'globalModals/SHOW_UUID_NOT_FOUND_MODAL';
|
||||
const SHOW_STORIES_SETTINGS = 'globalModals/SHOW_STORIES_SETTINGS';
|
||||
const HIDE_STORIES_SETTINGS = 'globalModals/HIDE_STORIES_SETTINGS';
|
||||
const TOGGLE_FORWARD_MESSAGE_MODAL =
|
||||
'globalModals/TOGGLE_FORWARD_MESSAGE_MODAL';
|
||||
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
||||
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
||||
'globalModals/TOGGLE_PROFILE_EDITOR_ERROR';
|
||||
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
|
||||
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
|
||||
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
|
||||
|
||||
export type ContactModalStateType = {
|
||||
contactId: string;
|
||||
|
@ -96,6 +102,18 @@ type ToggleSafetyNumberModalActionType = {
|
|||
payload: string | undefined;
|
||||
};
|
||||
|
||||
type ToggleSignalConnectionsModalActionType = {
|
||||
type: typeof TOGGLE_SIGNAL_CONNECTIONS_MODAL;
|
||||
};
|
||||
|
||||
type ShowStoriesSettingsActionType = {
|
||||
type: typeof SHOW_STORIES_SETTINGS;
|
||||
};
|
||||
|
||||
type HideStoriesSettingsActionType = {
|
||||
type: typeof HIDE_STORIES_SETTINGS;
|
||||
};
|
||||
|
||||
export type GlobalModalsActionType =
|
||||
| HideContactModalActionType
|
||||
| ShowContactModalActionType
|
||||
|
@ -103,10 +121,13 @@ export type GlobalModalsActionType =
|
|||
| ShowWhatsNewModalActionType
|
||||
| HideUserNotFoundModalActionType
|
||||
| ShowUserNotFoundModalActionType
|
||||
| HideStoriesSettingsActionType
|
||||
| ShowStoriesSettingsActionType
|
||||
| ToggleForwardMessageModalActionType
|
||||
| ToggleProfileEditorActionType
|
||||
| ToggleProfileEditorErrorActionType
|
||||
| ToggleSafetyNumberModalActionType;
|
||||
| ToggleSafetyNumberModalActionType
|
||||
| ToggleSignalConnectionsModalActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
|
@ -117,10 +138,13 @@ export const actions = {
|
|||
showWhatsNewModal,
|
||||
hideUserNotFoundModal,
|
||||
showUserNotFoundModal,
|
||||
hideStoriesSettings,
|
||||
showStoriesSettings,
|
||||
toggleForwardMessageModal,
|
||||
toggleProfileEditor,
|
||||
toggleProfileEditorHasError,
|
||||
toggleSafetyNumberModal,
|
||||
toggleSignalConnectionsModal,
|
||||
};
|
||||
|
||||
export const useGlobalModalActions = (): typeof actions =>
|
||||
|
@ -172,6 +196,14 @@ function showUserNotFoundModal(
|
|||
};
|
||||
}
|
||||
|
||||
function hideStoriesSettings(): HideStoriesSettingsActionType {
|
||||
return { type: HIDE_STORIES_SETTINGS };
|
||||
}
|
||||
|
||||
function showStoriesSettings(): ShowStoriesSettingsActionType {
|
||||
return { type: SHOW_STORIES_SETTINGS };
|
||||
}
|
||||
|
||||
function toggleForwardMessageModal(
|
||||
messageId?: string
|
||||
): ThunkAction<
|
||||
|
@ -224,13 +256,21 @@ function toggleSafetyNumberModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleSignalConnectionsModal(): ToggleSignalConnectionsModalActionType {
|
||||
return {
|
||||
type: TOGGLE_SIGNAL_CONNECTIONS_MODAL,
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): GlobalModalsStateType {
|
||||
return {
|
||||
isProfileEditorVisible: false,
|
||||
profileEditorHasError: false,
|
||||
isSignalConnectionsVisible: false,
|
||||
isStoriesSettingsVisible: false,
|
||||
isWhatsNewVisible: false,
|
||||
profileEditorHasError: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -310,5 +350,26 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === HIDE_STORIES_SETTINGS) {
|
||||
return {
|
||||
...state,
|
||||
isStoriesSettingsVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_STORIES_SETTINGS) {
|
||||
return {
|
||||
...state,
|
||||
isStoriesSettingsVisible: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_SIGNAL_CONNECTIONS_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
isSignalConnectionsVisible: !state.isSignalConnectionsVisible,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import type { StoryDistributionWithMembersType } from '../../sql/Interface';
|
||||
import type { UUIDStringType } from '../../types/UUID';
|
||||
import * as log from '../../logging/log';
|
||||
import dataInterface from '../../sql/Client';
|
||||
import { MY_STORIES_ID } from '../../types/Stories';
|
||||
import { UUID } from '../../types/UUID';
|
||||
import { replaceIndex } from '../../util/replaceIndex';
|
||||
import { storageServiceUploadJob } from '../../services/storage';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
||||
// State
|
||||
|
||||
export type StoryDistributionListDataType = {
|
||||
id: UUIDStringType;
|
||||
deletedAtTimestamp?: number;
|
||||
name: string;
|
||||
allowsReplies: boolean;
|
||||
isBlockList: boolean;
|
||||
memberUuids: Array<string>;
|
||||
};
|
||||
|
||||
export type StoryDistributionListStateType = {
|
||||
|
@ -19,36 +31,213 @@ export type StoryDistributionListStateType = {
|
|||
|
||||
// Actions
|
||||
|
||||
export const CREATE_LIST = 'storyDistributionLists/CREATE_LIST';
|
||||
export const MODIFY_LIST = 'storyDistributionLists/MODIFY_LIST';
|
||||
const ALLOW_REPLIES_CHANGED = 'storyDistributionLists/ALLOW_REPLIES_CHANGED';
|
||||
const CREATE_LIST = 'storyDistributionLists/CREATE_LIST';
|
||||
const DELETE_LIST = 'storyDistributionLists/DELETE_LIST';
|
||||
const HIDE_MY_STORIES_FROM = 'storyDistributionLists/HIDE_MY_STORIES_FROM';
|
||||
const MODIFY_LIST = 'storyDistributionLists/MODIFY_LIST';
|
||||
const REMOVE_MEMBER = 'storyDistributionLists/REMOVE_MEMBER';
|
||||
const RESET_MY_STORIES = 'storyDistributionLists/RESET_MY_STORIES';
|
||||
const VIEWERS_CHANGED = 'storyDistributionLists/VIEWERS_CHANGED';
|
||||
|
||||
type AllowRepliesChangedActionType = {
|
||||
type: typeof ALLOW_REPLIES_CHANGED;
|
||||
payload: {
|
||||
listId: string;
|
||||
allowsReplies: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type CreateListActionType = {
|
||||
type: typeof CREATE_LIST;
|
||||
payload: StoryDistributionListDataType;
|
||||
};
|
||||
|
||||
type DeleteListActionType = {
|
||||
type: typeof DELETE_LIST;
|
||||
payload: {
|
||||
listId: string;
|
||||
deletedAtTimestamp: number;
|
||||
};
|
||||
};
|
||||
|
||||
type HideMyStoriesFromActionType = {
|
||||
type: typeof HIDE_MY_STORIES_FROM;
|
||||
payload: Array<string>;
|
||||
};
|
||||
|
||||
type ModifyDistributionListType = Omit<
|
||||
StoryDistributionListDataType,
|
||||
'memberUuids'
|
||||
> & {
|
||||
membersToAdd: Array<string>;
|
||||
membersToRemove: Array<string>;
|
||||
};
|
||||
|
||||
export type ModifyListActionType = {
|
||||
type: typeof MODIFY_LIST;
|
||||
payload: StoryDistributionListDataType;
|
||||
payload: ModifyDistributionListType;
|
||||
};
|
||||
|
||||
type RemoveMemberActionType = {
|
||||
type: typeof REMOVE_MEMBER;
|
||||
payload: {
|
||||
listId: string;
|
||||
memberUuid: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ResetMyStoriesActionType = {
|
||||
type: typeof RESET_MY_STORIES;
|
||||
};
|
||||
|
||||
type ViewersChangedActionType = {
|
||||
type: typeof VIEWERS_CHANGED;
|
||||
payload: {
|
||||
listId: string;
|
||||
memberUuids: Array<string>;
|
||||
};
|
||||
};
|
||||
|
||||
type StoryDistributionListsActionType =
|
||||
| AllowRepliesChangedActionType
|
||||
| CreateListActionType
|
||||
| ModifyListActionType;
|
||||
| DeleteListActionType
|
||||
| HideMyStoriesFromActionType
|
||||
| ModifyListActionType
|
||||
| RemoveMemberActionType
|
||||
| ResetMyStoriesActionType
|
||||
| ViewersChangedActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
function allowsRepliesChanged(
|
||||
listId: string,
|
||||
allowsReplies: boolean
|
||||
): ThunkAction<void, RootStateType, null, AllowRepliesChangedActionType> {
|
||||
return async dispatch => {
|
||||
const storyDistribution =
|
||||
await dataInterface.getStoryDistributionWithMembers(listId);
|
||||
|
||||
if (!storyDistribution) {
|
||||
log.warn(
|
||||
'storyDistributionLists.allowsRepliesChanged: No story found for id',
|
||||
listId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (storyDistribution.allowsReplies === allowsReplies) {
|
||||
log.warn(
|
||||
'storyDistributionLists.allowsRepliesChanged: story already has the same value',
|
||||
{ listId, allowsReplies }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await dataInterface.modifyStoryDistribution({
|
||||
...storyDistribution,
|
||||
allowsReplies,
|
||||
storageNeedsSync: true,
|
||||
});
|
||||
|
||||
storageServiceUploadJob();
|
||||
|
||||
log.info(
|
||||
'storyDistributionLists.allowsRepliesChanged: allowsReplies has changed',
|
||||
listId
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: ALLOW_REPLIES_CHANGED,
|
||||
payload: {
|
||||
listId,
|
||||
allowsReplies,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createDistributionList(
|
||||
distributionList: StoryDistributionListDataType
|
||||
): CreateListActionType {
|
||||
return {
|
||||
type: CREATE_LIST,
|
||||
payload: distributionList,
|
||||
name: string,
|
||||
memberUuids: Array<UUIDStringType>,
|
||||
storageServiceDistributionListRecord?: StoryDistributionWithMembersType,
|
||||
shouldSave = true
|
||||
): ThunkAction<void, RootStateType, null, CreateListActionType> {
|
||||
return async dispatch => {
|
||||
const storyDistribution: StoryDistributionWithMembersType = {
|
||||
allowsReplies: true,
|
||||
id: UUID.generate().toString(),
|
||||
isBlockList: false,
|
||||
members: memberUuids,
|
||||
name,
|
||||
senderKeyInfo: undefined,
|
||||
storageNeedsSync: true,
|
||||
...(storageServiceDistributionListRecord || {}),
|
||||
};
|
||||
|
||||
if (shouldSave) {
|
||||
await dataInterface.createNewStoryDistribution(storyDistribution);
|
||||
}
|
||||
|
||||
if (storyDistribution.storageNeedsSync) {
|
||||
storageServiceUploadJob();
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CREATE_LIST,
|
||||
payload: {
|
||||
allowsReplies: Boolean(storyDistribution.allowsReplies),
|
||||
deletedAtTimestamp: storyDistribution.deletedAtTimestamp,
|
||||
id: storyDistribution.id,
|
||||
isBlockList: Boolean(storyDistribution.isBlockList),
|
||||
memberUuids,
|
||||
name: storyDistribution.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function deleteDistributionList(
|
||||
listId: string
|
||||
): ThunkAction<void, RootStateType, unknown, DeleteListActionType> {
|
||||
return async dispatch => {
|
||||
const deletedAtTimestamp = Date.now();
|
||||
|
||||
const storyDistribution =
|
||||
await dataInterface.getStoryDistributionWithMembers(listId);
|
||||
|
||||
if (!storyDistribution) {
|
||||
log.warn('No story distribution found for id', listId);
|
||||
return;
|
||||
}
|
||||
|
||||
await dataInterface.modifyStoryDistribution({
|
||||
...storyDistribution,
|
||||
deletedAtTimestamp,
|
||||
name: '',
|
||||
storageNeedsSync: true,
|
||||
});
|
||||
|
||||
log.info(
|
||||
'storyDistributionLists.deleteDistributionList: list deleted',
|
||||
listId
|
||||
);
|
||||
|
||||
storageServiceUploadJob();
|
||||
|
||||
dispatch({
|
||||
type: DELETE_LIST,
|
||||
payload: {
|
||||
listId,
|
||||
deletedAtTimestamp,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function modifyDistributionList(
|
||||
distributionList: StoryDistributionListDataType
|
||||
distributionList: ModifyDistributionListType
|
||||
): ModifyListActionType {
|
||||
return {
|
||||
type: MODIFY_LIST,
|
||||
|
@ -56,9 +245,206 @@ function modifyDistributionList(
|
|||
};
|
||||
}
|
||||
|
||||
function hideMyStoriesFrom(
|
||||
memberUuids: Array<UUIDStringType>
|
||||
): ThunkAction<void, RootStateType, null, HideMyStoriesFromActionType> {
|
||||
return async dispatch => {
|
||||
const myStories = await dataInterface.getStoryDistributionWithMembers(
|
||||
MY_STORIES_ID
|
||||
);
|
||||
|
||||
if (!myStories) {
|
||||
log.error(
|
||||
'storyDistributionLists.hideMyStoriesFrom: Could not find My Stories!'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const toAdd = new Set<UUIDStringType>(memberUuids);
|
||||
|
||||
await dataInterface.modifyStoryDistributionWithMembers(
|
||||
{
|
||||
...myStories,
|
||||
isBlockList: true,
|
||||
storageNeedsSync: true,
|
||||
},
|
||||
{
|
||||
toAdd: Array.from(toAdd),
|
||||
toRemove: myStories.members.filter(uuid => !toAdd.has(uuid)),
|
||||
}
|
||||
);
|
||||
|
||||
storageServiceUploadJob();
|
||||
|
||||
dispatch({
|
||||
type: HIDE_MY_STORIES_FROM,
|
||||
payload: memberUuids,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function removeMemberFromDistributionList(
|
||||
listId: string,
|
||||
memberUuid: UUIDStringType | undefined
|
||||
): ThunkAction<void, RootStateType, null, RemoveMemberActionType> {
|
||||
return async dispatch => {
|
||||
if (!memberUuid) {
|
||||
log.warn(
|
||||
'storyDistributionLists.removeMemberFromDistributionList cannot remove a member without uuid',
|
||||
listId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const storyDistribution =
|
||||
await dataInterface.getStoryDistributionWithMembers(listId);
|
||||
|
||||
if (!storyDistribution) {
|
||||
log.warn(
|
||||
'storyDistributionLists.removeMemberFromDistributionList: No story found for id',
|
||||
listId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await dataInterface.modifyStoryDistributionWithMembers(
|
||||
{
|
||||
...storyDistribution,
|
||||
storageNeedsSync: true,
|
||||
},
|
||||
{
|
||||
toAdd: [],
|
||||
toRemove: [memberUuid],
|
||||
}
|
||||
);
|
||||
|
||||
log.info(
|
||||
'storyDistributionLists.removeMemberFromDistributionList: removed',
|
||||
{
|
||||
listId,
|
||||
memberUuid,
|
||||
}
|
||||
);
|
||||
|
||||
storageServiceUploadJob();
|
||||
|
||||
dispatch({
|
||||
type: REMOVE_MEMBER,
|
||||
payload: {
|
||||
listId,
|
||||
memberUuid,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setMyStoriesToAllSignalConnections(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
null,
|
||||
ResetMyStoriesActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
const myStories = await dataInterface.getStoryDistributionWithMembers(
|
||||
MY_STORIES_ID
|
||||
);
|
||||
|
||||
if (!myStories) {
|
||||
log.error(
|
||||
'storyDistributionLists.setMyStoriesToAllSignalConnections: Could not find My Stories!'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (myStories.isBlockList || myStories.members.length > 0) {
|
||||
await dataInterface.modifyStoryDistributionWithMembers(
|
||||
{
|
||||
...myStories,
|
||||
isBlockList: true,
|
||||
storageNeedsSync: true,
|
||||
},
|
||||
{
|
||||
toAdd: [],
|
||||
toRemove: myStories.members,
|
||||
}
|
||||
);
|
||||
|
||||
storageServiceUploadJob();
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: RESET_MY_STORIES,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateStoryViewers(
|
||||
listId: string,
|
||||
memberUuids: Array<UUIDStringType>
|
||||
): ThunkAction<void, RootStateType, null, ViewersChangedActionType> {
|
||||
return async dispatch => {
|
||||
const storyDistribution =
|
||||
await dataInterface.getStoryDistributionWithMembers(listId);
|
||||
|
||||
if (!storyDistribution) {
|
||||
log.warn(
|
||||
'storyDistributionLists.updateStoryViewers: No story found for id',
|
||||
listId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingUuids = new Set<UUIDStringType>(storyDistribution.members);
|
||||
const toAdd: Array<UUIDStringType> = [];
|
||||
|
||||
memberUuids.forEach(uuid => {
|
||||
if (!existingUuids.has(uuid)) {
|
||||
toAdd.push(uuid);
|
||||
}
|
||||
});
|
||||
|
||||
const updatedUuids = new Set<UUIDStringType>(memberUuids);
|
||||
const toRemove: Array<UUIDStringType> = [];
|
||||
|
||||
storyDistribution.members.forEach(uuid => {
|
||||
if (!updatedUuids.has(uuid)) {
|
||||
toRemove.push(uuid);
|
||||
}
|
||||
});
|
||||
|
||||
await dataInterface.modifyStoryDistributionWithMembers(
|
||||
{
|
||||
...storyDistribution,
|
||||
isBlockList: false,
|
||||
storageNeedsSync: true,
|
||||
},
|
||||
{
|
||||
toAdd,
|
||||
toRemove,
|
||||
}
|
||||
);
|
||||
|
||||
storageServiceUploadJob();
|
||||
|
||||
dispatch({
|
||||
type: VIEWERS_CHANGED,
|
||||
payload: {
|
||||
listId,
|
||||
memberUuids,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
allowsRepliesChanged,
|
||||
createDistributionList,
|
||||
deleteDistributionList,
|
||||
hideMyStoriesFrom,
|
||||
modifyDistributionList,
|
||||
removeMemberFromDistributionList,
|
||||
setMyStoriesToAllSignalConnections,
|
||||
updateStoryViewers,
|
||||
};
|
||||
|
||||
export const useStoryDistributionListsActions = (): typeof actions =>
|
||||
|
@ -72,23 +458,61 @@ export function getEmptyState(): StoryDistributionListStateType {
|
|||
};
|
||||
}
|
||||
|
||||
function replaceDistributionListData(
|
||||
distributionLists: Array<StoryDistributionListDataType>,
|
||||
listId: string,
|
||||
getNextDistributionListData: (
|
||||
list: StoryDistributionListDataType
|
||||
) => Partial<StoryDistributionListDataType>
|
||||
): Array<StoryDistributionListDataType> | undefined {
|
||||
const listIndex = distributionLists.findIndex(list => list.id === listId);
|
||||
|
||||
if (listIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return replaceIndex(distributionLists, listIndex, {
|
||||
...distributionLists[listIndex],
|
||||
...getNextDistributionListData(distributionLists[listIndex]),
|
||||
});
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<StoryDistributionListStateType> = getEmptyState(),
|
||||
action: Readonly<StoryDistributionListsActionType>
|
||||
): StoryDistributionListStateType {
|
||||
if (action.type === MODIFY_LIST) {
|
||||
const { payload } = action;
|
||||
const distributionLists = [...state.distributionLists];
|
||||
|
||||
const existingList = distributionLists.find(list => list.id === payload.id);
|
||||
if (existingList) {
|
||||
Object.assign(existingList, payload);
|
||||
} else {
|
||||
distributionLists.concat(payload);
|
||||
const { membersToAdd, membersToRemove, ...distributionListDetails } =
|
||||
payload;
|
||||
|
||||
const listIndex = state.distributionLists.findIndex(
|
||||
list => list.id === distributionListDetails.id
|
||||
);
|
||||
if (listIndex >= 0) {
|
||||
const existingDistributionList = state.distributionLists[listIndex];
|
||||
const memberUuids = new Set<string>(existingDistributionList.memberUuids);
|
||||
membersToAdd.forEach(uuid => memberUuids.add(uuid));
|
||||
membersToRemove.forEach(uuid => memberUuids.delete(uuid));
|
||||
|
||||
return {
|
||||
distributionLists: replaceIndex(state.distributionLists, listIndex, {
|
||||
...existingDistributionList,
|
||||
...distributionListDetails,
|
||||
memberUuids: Array.from(memberUuids),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
distributionLists: [...distributionLists],
|
||||
distributionLists: [
|
||||
...state.distributionLists,
|
||||
{
|
||||
...distributionListDetails,
|
||||
memberUuids: membersToAdd,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -98,5 +522,83 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === DELETE_LIST) {
|
||||
const distributionLists = replaceDistributionListData(
|
||||
state.distributionLists,
|
||||
action.payload.listId,
|
||||
() => ({
|
||||
deletedAtTimestamp: action.payload.deletedAtTimestamp,
|
||||
name: '',
|
||||
})
|
||||
);
|
||||
|
||||
return distributionLists ? { distributionLists } : state;
|
||||
}
|
||||
|
||||
if (action.type === HIDE_MY_STORIES_FROM) {
|
||||
const distributionLists = replaceDistributionListData(
|
||||
state.distributionLists,
|
||||
MY_STORIES_ID,
|
||||
() => ({
|
||||
isBlockList: true,
|
||||
memberUuids: action.payload,
|
||||
})
|
||||
);
|
||||
|
||||
return distributionLists ? { distributionLists } : state;
|
||||
}
|
||||
|
||||
if (action.type === REMOVE_MEMBER) {
|
||||
const distributionLists = replaceDistributionListData(
|
||||
state.distributionLists,
|
||||
action.payload.listId,
|
||||
list => ({
|
||||
memberUuids: list.memberUuids.filter(
|
||||
uuid => uuid !== action.payload.memberUuid
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
return distributionLists ? { distributionLists } : state;
|
||||
}
|
||||
|
||||
if (action.type === ALLOW_REPLIES_CHANGED) {
|
||||
const distributionLists = replaceDistributionListData(
|
||||
state.distributionLists,
|
||||
action.payload.listId,
|
||||
() => ({
|
||||
allowsReplies: action.payload.allowsReplies,
|
||||
})
|
||||
);
|
||||
|
||||
return distributionLists ? { distributionLists } : state;
|
||||
}
|
||||
|
||||
if (action.type === VIEWERS_CHANGED) {
|
||||
const distributionLists = replaceDistributionListData(
|
||||
state.distributionLists,
|
||||
action.payload.listId,
|
||||
() => ({
|
||||
isBlockList: false,
|
||||
memberUuids: Array.from(new Set(action.payload.memberUuids)),
|
||||
})
|
||||
);
|
||||
|
||||
return distributionLists ? { distributionLists } : state;
|
||||
}
|
||||
|
||||
if (action.type === RESET_MY_STORIES) {
|
||||
const distributionLists = replaceDistributionListData(
|
||||
state.distributionLists,
|
||||
MY_STORIES_ID,
|
||||
() => ({
|
||||
isBlockList: false,
|
||||
memberUuids: [],
|
||||
})
|
||||
);
|
||||
|
||||
return distributionLists ? { distributionLists } : state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -5,14 +5,31 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { StoryDistributionListDataType } from '../ducks/storyDistributionLists';
|
||||
import type { StoryDistributionListWithMembersDataType } from '../../types/Stories';
|
||||
import { getConversationSelector } from './conversations';
|
||||
|
||||
const getDistributionLists = (
|
||||
export const getDistributionLists = (
|
||||
state: StateType
|
||||
): Array<StoryDistributionListDataType> =>
|
||||
state.storyDistributionLists.distributionLists;
|
||||
state.storyDistributionLists.distributionLists.filter(
|
||||
list => !list.deletedAtTimestamp
|
||||
);
|
||||
|
||||
export const getDistributionListSelector = createSelector(
|
||||
getDistributionLists,
|
||||
distributionLists => (id: string) =>
|
||||
distributionLists.find(list => list.id === id)
|
||||
);
|
||||
|
||||
export const getDistributionListsWithMembers = createSelector(
|
||||
getConversationSelector,
|
||||
getDistributionLists,
|
||||
(
|
||||
conversationSelector,
|
||||
distributionLists
|
||||
): Array<StoryDistributionListWithMembersDataType> =>
|
||||
distributionLists.map(list => ({
|
||||
...list,
|
||||
members: list.memberUuids.map(uuid => conversationSelector(uuid)),
|
||||
}))
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { SmartContactModal } from './ContactModal';
|
|||
import { SmartForwardMessageModal } from './ForwardMessageModal';
|
||||
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
||||
import { SmartSafetyNumberModal } from './SafetyNumberModal';
|
||||
import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
|
@ -25,6 +26,10 @@ function renderForwardMessageModal(): JSX.Element {
|
|||
return <SmartForwardMessageModal />;
|
||||
}
|
||||
|
||||
function renderStoriesSettings(): JSX.Element {
|
||||
return <SmartStoriesSettingsModal />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const i18n = getIntl(state);
|
||||
|
||||
|
@ -34,6 +39,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
renderContactModal,
|
||||
renderForwardMessageModal,
|
||||
renderProfileEditor,
|
||||
renderStoriesSettings,
|
||||
renderSafetyNumber: () => (
|
||||
<SmartSafetyNumberModal
|
||||
contactID={String(state.globalModals.safetyNumberModalContactId)}
|
||||
|
|
|
@ -27,7 +27,8 @@ function renderStoryCreator({
|
|||
export function SmartStories(): JSX.Element | null {
|
||||
const storiesActions = useStoriesActions();
|
||||
const { showConversation, toggleHideStories } = useConversationsActions();
|
||||
const { toggleForwardMessageModal } = useGlobalModalActions();
|
||||
const { showStoriesSettings, toggleForwardMessageModal } =
|
||||
useGlobalModalActions();
|
||||
|
||||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||
|
||||
|
@ -64,6 +65,7 @@ export function SmartStories(): JSX.Element | null {
|
|||
preferredWidthFromStorage={preferredWidthFromStorage}
|
||||
renderStoryCreator={renderStoryCreator}
|
||||
showConversation={showConversation}
|
||||
showStoriesSettings={showStoriesSettings}
|
||||
stories={stories}
|
||||
toggleHideStories={toggleHideStories}
|
||||
{...storiesActions}
|
||||
|
|
58
ts/state/smart/StoriesSettingsModal.tsx
Normal file
58
ts/state/smart/StoriesSettingsModal.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { StateType } from '../reducer';
|
||||
import { StoriesSettingsModal } from '../../components/StoriesSettingsModal';
|
||||
import {
|
||||
getCandidateContactsForNewGroup,
|
||||
getMe,
|
||||
} from '../selectors/conversations';
|
||||
import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
|
||||
|
||||
export function SmartStoriesSettingsModal(): JSX.Element | null {
|
||||
const { hideStoriesSettings, toggleSignalConnectionsModal } =
|
||||
useGlobalModalActions();
|
||||
const {
|
||||
allowsRepliesChanged,
|
||||
createDistributionList,
|
||||
deleteDistributionList,
|
||||
hideMyStoriesFrom,
|
||||
removeMemberFromDistributionList,
|
||||
setMyStoriesToAllSignalConnections,
|
||||
updateStoryViewers,
|
||||
} = useStoryDistributionListsActions();
|
||||
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||
const me = useSelector(getMe);
|
||||
|
||||
const candidateConversations = useSelector(getCandidateContactsForNewGroup);
|
||||
const distributionLists = useSelector(getDistributionListsWithMembers);
|
||||
|
||||
return (
|
||||
<StoriesSettingsModal
|
||||
candidateConversations={candidateConversations}
|
||||
distributionLists={distributionLists}
|
||||
hideStoriesSettings={hideStoriesSettings}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
me={me}
|
||||
onDeleteList={deleteDistributionList}
|
||||
onDistributionListCreated={createDistributionList}
|
||||
onHideMyStoriesFrom={hideMyStoriesFrom}
|
||||
onRemoveMember={removeMemberFromDistributionList}
|
||||
onRepliesNReactionsChanged={allowsRepliesChanged}
|
||||
onViewersUpdated={updateStoryViewers}
|
||||
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,10 @@ import createDebug from 'debug';
|
|||
import * as durations from '../../util/durations';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
import { MY_STORIES_ID } from '../../types/Stories';
|
||||
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||
|
||||
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||
|
||||
export const debug = createDebug('mock:test:gv2');
|
||||
|
||||
|
@ -58,6 +62,19 @@ describe('gv2', function needsName() {
|
|||
givenName: 'PNI Contact',
|
||||
});
|
||||
|
||||
state = state.addRecord({
|
||||
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
||||
record: {
|
||||
storyDistributionList: {
|
||||
allowsReplies: true,
|
||||
identifier: uuidToBytes(MY_STORIES_ID),
|
||||
isBlockList: true,
|
||||
name: MY_STORIES_ID,
|
||||
recipientUuids: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await phone.setStorageState(state);
|
||||
|
||||
app = await bootstrap.link();
|
||||
|
|
|
@ -7,6 +7,8 @@ import { StorageState, Proto } from '@signalapp/mock-server';
|
|||
import { App } from '../playwright';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { BootstrapOptions } from '../bootstrap';
|
||||
import { MY_STORIES_ID } from '../../types/Stories';
|
||||
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||
|
||||
export const debug = createDebug('mock:test:storage');
|
||||
|
||||
|
@ -14,6 +16,8 @@ export { App, Bootstrap };
|
|||
|
||||
const GROUP_SIZE = 8;
|
||||
|
||||
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||
|
||||
export type InitStorageResultType = Readonly<{
|
||||
bootstrap: Bootstrap;
|
||||
app: App;
|
||||
|
@ -77,6 +81,19 @@ export async function initStorage(
|
|||
|
||||
state = state.pin(firstContact);
|
||||
|
||||
state = state.addRecord({
|
||||
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
||||
record: {
|
||||
storyDistributionList: {
|
||||
allowsReplies: true,
|
||||
identifier: uuidToBytes(MY_STORIES_ID),
|
||||
isBlockList: true,
|
||||
name: MY_STORIES_ID,
|
||||
recipientUuids: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await phone.setStorageState(state);
|
||||
|
||||
// Link new device
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
import type { AttachmentType } from './Attachment';
|
||||
import type { ContactNameColorType } from './Colors';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from './Util';
|
||||
import type { SendStatus } from '../messages/MessageSendState';
|
||||
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
||||
|
||||
export type ReplyType = Pick<
|
||||
ConversationType,
|
||||
|
@ -110,3 +112,18 @@ export enum StoryViewModeType {
|
|||
All = 'All',
|
||||
Single = 'Single',
|
||||
}
|
||||
|
||||
export type StoryDistributionListWithMembersDataType = Omit<
|
||||
StoryDistributionListDataType,
|
||||
'memberUuids'
|
||||
> & {
|
||||
members: Array<ConversationType>;
|
||||
};
|
||||
|
||||
export function getStoryDistributionListName(
|
||||
i18n: LocalizerType,
|
||||
id: string,
|
||||
name: string
|
||||
): string {
|
||||
return id === MY_STORIES_ID ? i18n('Stories__mine') : name;
|
||||
}
|
||||
|
|
16
ts/util/shouldNeverBeCalled.ts
Normal file
16
ts/util/shouldNeverBeCalled.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from './assert';
|
||||
|
||||
export function shouldNeverBeCalled(..._args: ReadonlyArray<unknown>): void {
|
||||
assert(false, 'This should never be called. Doing nothing');
|
||||
}
|
||||
|
||||
export async function asyncShouldNeverBeCalled(
|
||||
..._args: ReadonlyArray<unknown>
|
||||
): Promise<undefined> {
|
||||
shouldNeverBeCalled();
|
||||
|
||||
return undefined;
|
||||
}
|
19
ts/util/uuidToBytes.ts
Normal file
19
ts/util/uuidToBytes.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { chunk } from 'lodash';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
export function uuidToBytes(uuid: string): Uint8Array {
|
||||
if (uuid.length !== 36) {
|
||||
log.warn(
|
||||
'uuidToBytes: received a string of invalid length. ' +
|
||||
'Returning an empty Uint8Array'
|
||||
);
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
return Uint8Array.from(
|
||||
chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16))
|
||||
);
|
||||
}
|
|
@ -1753,10 +1753,10 @@
|
|||
node-gyp-build "^4.2.3"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@signalapp/mock-server@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.0.1.tgz#0ecee7a0060181546e6b0c1b8e8c6f361fb2d7fe"
|
||||
integrity sha512-YB0MIUzW8D1NirKpxxNXgEYuvK/OWbFo3djsBA4GqEUBIsJmdYcd4auHSqV3gKE/eSRoFQ0Z//eJNiqtsHbSEw==
|
||||
"@signalapp/mock-server@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.1.0.tgz#25e42aad9ec2bc76c92173e7894f1aec4c2bb719"
|
||||
integrity sha512-AoeCRw8hOv4F+YQ6um/ZZiskaS1SsAXoQPgSMK69/xfDcPURJnVU6KB5Fy3chU2ZF0SZyWzS8vF3QguFKsIFWA==
|
||||
dependencies:
|
||||
"@signalapp/libsignal-client" "^0.18.1"
|
||||
debug "^4.3.2"
|
||||
|
|
Loading…
Reference in a new issue