1000 lines
32 KiB
TypeScript
1000 lines
32 KiB
TypeScript
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { noop, sortBy } from 'lodash';
|
|
|
|
import { SearchInput } from './SearchInput';
|
|
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
|
|
|
import type { ConversationType } from '../state/ducks/conversations';
|
|
import type { LocalizerType } from '../types/Util';
|
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|
import type { PropsType as StoriesSettingsModalPropsType } from './StoriesSettingsModal';
|
|
import {
|
|
getI18nForMyStory,
|
|
getListViewers,
|
|
DistributionListSettingsModal,
|
|
EditDistributionListModal,
|
|
EditMyStoryPrivacy,
|
|
Page as StoriesSettingsPage,
|
|
} from './StoriesSettingsModal';
|
|
import type { StoryDistributionListWithMembersDataType } from '../types/Stories';
|
|
import type { UUIDStringType } from '../types/UUID';
|
|
import { Alert } from './Alert';
|
|
import { Avatar, AvatarSize } from './Avatar';
|
|
import { Button, ButtonSize, ButtonVariant } from './Button';
|
|
import { Checkbox } from './Checkbox';
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
import { ContextMenu } from './ContextMenu';
|
|
|
|
import { MY_STORY_ID, getStoryDistributionListName } from '../types/Stories';
|
|
import type { RenderModalPage, ModalPropsType } from './Modal';
|
|
import { PagedModal, ModalPage } from './Modal';
|
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
|
import { Theme } from '../util/theme';
|
|
import { isNotNil } from '../util/isNotNil';
|
|
import { StoryImage } from './StoryImage';
|
|
import type { AttachmentType } from '../types/Attachment';
|
|
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
|
import { getStoryBackground } from '../util/getStoryBackground';
|
|
import { makeObjectUrl, revokeObjectUrl } from '../types/VisualAttachment';
|
|
|
|
export type PropsType = {
|
|
draftAttachment: AttachmentType;
|
|
candidateConversations: Array<ConversationType>;
|
|
distributionLists: Array<StoryDistributionListWithMembersDataType>;
|
|
getPreferredBadge: PreferredBadgeSelectorType;
|
|
groupConversations: Array<ConversationType>;
|
|
groupStories: Array<ConversationType>;
|
|
hasFirstStoryPostExperience: boolean;
|
|
ourConversationId: string | undefined;
|
|
i18n: LocalizerType;
|
|
me: ConversationType;
|
|
onClose: () => unknown;
|
|
onDeleteList: (listId: string) => unknown;
|
|
onDistributionListCreated: (
|
|
name: string,
|
|
viewerUuids: Array<UUIDStringType>
|
|
) => Promise<UUIDStringType>;
|
|
onSelectedStoryList: (options: {
|
|
conversationId: string;
|
|
distributionId: string | undefined;
|
|
uuids: Array<UUIDStringType>;
|
|
}) => unknown;
|
|
onSend: (
|
|
listIds: Array<UUIDStringType>,
|
|
conversationIds: Array<string>
|
|
) => unknown;
|
|
signalConnections: Array<ConversationType>;
|
|
toggleGroupsForStorySend: (cids: Array<string>) => Promise<void>;
|
|
mostRecentActiveStoryTimestampByGroupOrDistributionList: Record<
|
|
string,
|
|
number
|
|
>;
|
|
} & Pick<
|
|
StoriesSettingsModalPropsType,
|
|
| 'onHideMyStoriesFrom'
|
|
| 'onRemoveMembers'
|
|
| 'onRepliesNReactionsChanged'
|
|
| 'onViewersUpdated'
|
|
| 'setMyStoriesToAllSignalConnections'
|
|
| 'toggleSignalConnectionsModal'
|
|
>;
|
|
|
|
enum SendStoryPage {
|
|
ChooseGroups = 'ChooseGroups',
|
|
EditingDistributionList = 'EditingDistributionList',
|
|
SendStory = 'SendStory',
|
|
SetMyStoriesPrivacy = 'SetMyStoriesPrivacy',
|
|
}
|
|
|
|
const Page = {
|
|
...SendStoryPage,
|
|
...StoriesSettingsPage,
|
|
};
|
|
|
|
type PageType = SendStoryPage | StoriesSettingsPage;
|
|
|
|
function getListMemberUuids(
|
|
list: StoryDistributionListWithMembersDataType,
|
|
signalConnections: Array<ConversationType>
|
|
): Array<UUIDStringType> {
|
|
const memberUuids = list.members.map(({ uuid }) => uuid).filter(isNotNil);
|
|
|
|
if (list.id === MY_STORY_ID && list.isBlockList) {
|
|
const excludeUuids = new Set<string>(memberUuids);
|
|
return signalConnections
|
|
.map(conversation => conversation.uuid)
|
|
.filter(isNotNil)
|
|
.filter(uuid => !excludeUuids.has(uuid));
|
|
}
|
|
|
|
return memberUuids;
|
|
}
|
|
|
|
export function SendStoryModal({
|
|
draftAttachment,
|
|
candidateConversations,
|
|
distributionLists,
|
|
getPreferredBadge,
|
|
groupConversations,
|
|
groupStories,
|
|
hasFirstStoryPostExperience,
|
|
i18n,
|
|
me,
|
|
ourConversationId,
|
|
onClose,
|
|
onDeleteList,
|
|
onDistributionListCreated,
|
|
onHideMyStoriesFrom,
|
|
onRemoveMembers,
|
|
onRepliesNReactionsChanged,
|
|
onSelectedStoryList,
|
|
onSend,
|
|
onViewersUpdated,
|
|
setMyStoriesToAllSignalConnections,
|
|
signalConnections,
|
|
toggleGroupsForStorySend,
|
|
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
|
toggleSignalConnectionsModal,
|
|
}: PropsType): JSX.Element {
|
|
const [page, setPage] = useState<PageType>(Page.SendStory);
|
|
|
|
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
|
|
|
|
const [selectedListIds, setSelectedListIds] = useState<Set<UUIDStringType>>(
|
|
new Set()
|
|
);
|
|
const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>(
|
|
new Set()
|
|
);
|
|
const selectedStoryNames = useMemo(
|
|
() =>
|
|
distributionLists
|
|
.filter(list => selectedListIds.has(list.id))
|
|
.map(list => list.name)
|
|
.concat(
|
|
groupStories
|
|
.filter(group => selectedGroupIds.has(group.id))
|
|
.map(group => group.title)
|
|
),
|
|
[distributionLists, groupStories, selectedGroupIds, selectedListIds]
|
|
);
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
const [filteredConversations, setFilteredConversations] = useState(
|
|
filterAndSortConversationsByRecent(
|
|
groupConversations,
|
|
searchTerm,
|
|
undefined
|
|
)
|
|
);
|
|
|
|
const normalizedSearchTerm = searchTerm.trim();
|
|
|
|
useEffect(() => {
|
|
const timeout = setTimeout(() => {
|
|
setFilteredConversations(
|
|
filterAndSortConversationsByRecent(
|
|
groupConversations,
|
|
normalizedSearchTerm,
|
|
undefined
|
|
)
|
|
);
|
|
}, 200);
|
|
return () => {
|
|
clearTimeout(timeout);
|
|
};
|
|
}, [groupConversations, normalizedSearchTerm, setFilteredConversations]);
|
|
|
|
const [chosenGroupIds, setChosenGroupIds] = useState<Set<string>>(
|
|
new Set<string>()
|
|
);
|
|
|
|
const chosenGroupNames = useMemo(
|
|
() =>
|
|
filteredConversations
|
|
.filter(group => chosenGroupIds.has(group.id))
|
|
.map(group => group.title),
|
|
[filteredConversations, chosenGroupIds]
|
|
);
|
|
|
|
const [selectedContacts, setSelectedContacts] = useState<
|
|
Array<ConversationType>
|
|
>([]);
|
|
|
|
const [hasAnnouncementsOnlyAlert, setHasAnnouncementsOnlyAlert] =
|
|
useState(false);
|
|
const [confirmRemoveGroupId, setConfirmRemoveGroupId] = useState<
|
|
string | undefined
|
|
>();
|
|
const [confirmDeleteList, setConfirmDeleteList] = useState<
|
|
{ id: string; name: string } | undefined
|
|
>();
|
|
|
|
const [listIdToEdit, setListIdToEdit] = useState<string | undefined>();
|
|
|
|
useEffect(() => {
|
|
if (listIdToEdit) {
|
|
setPage(Page.EditingDistributionList);
|
|
} else {
|
|
setPage(Page.SendStory);
|
|
}
|
|
}, [listIdToEdit]);
|
|
|
|
const listToEdit = useMemo(() => {
|
|
if (!listIdToEdit) {
|
|
return;
|
|
}
|
|
|
|
return distributionLists.find(list => list.id === listIdToEdit);
|
|
}, [distributionLists, listIdToEdit]);
|
|
|
|
// myStoriesPrivacy, myStoriesPrivacyUuids, and myStories are only used
|
|
// during the first time posting to My Stories experience where we have
|
|
// to select the privacy settings.
|
|
const ogMyStories = useMemo(
|
|
() => distributionLists.find(list => list.id === MY_STORY_ID),
|
|
[distributionLists]
|
|
);
|
|
|
|
const initialMyStories: StoryDistributionListWithMembersDataType = useMemo(
|
|
() => ({
|
|
allowsReplies: true,
|
|
id: MY_STORY_ID,
|
|
name: i18n('Stories__mine'),
|
|
isBlockList: ogMyStories?.isBlockList ?? true,
|
|
members: ogMyStories?.members || [],
|
|
}),
|
|
[i18n, ogMyStories]
|
|
);
|
|
|
|
const initialMyStoriesMemberUuids = useMemo(
|
|
() => (ogMyStories?.members || []).map(({ uuid }) => uuid).filter(isNotNil),
|
|
[ogMyStories]
|
|
);
|
|
|
|
const [stagedMyStories, setStagedMyStories] =
|
|
useState<StoryDistributionListWithMembersDataType>(initialMyStories);
|
|
|
|
const [stagedMyStoriesMemberUuids, setStagedMyStoriesMemberUuids] = useState<
|
|
Array<UUIDStringType>
|
|
>(initialMyStoriesMemberUuids);
|
|
|
|
let selectedNames: string | undefined;
|
|
if (page === Page.ChooseGroups) {
|
|
selectedNames = chosenGroupNames.join(', ');
|
|
} else {
|
|
selectedNames = selectedStoryNames
|
|
.map(listName => getStoryDistributionListName(i18n, listName, listName))
|
|
.join(', ');
|
|
}
|
|
|
|
const [objectUrl, setObjectUrl] = useState<string | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
let url: undefined | string;
|
|
|
|
if (draftAttachment.url) {
|
|
setObjectUrl(draftAttachment.url);
|
|
} else if (draftAttachment.data) {
|
|
url = makeObjectUrl(draftAttachment.data, draftAttachment.contentType);
|
|
setObjectUrl(url);
|
|
}
|
|
return () => {
|
|
if (url) {
|
|
revokeObjectUrl(url);
|
|
}
|
|
};
|
|
}, [setObjectUrl, draftAttachment]);
|
|
|
|
const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'i18n'> = {
|
|
hasXButton: true,
|
|
i18n,
|
|
};
|
|
|
|
let modal: RenderModalPage;
|
|
if (page === Page.SetMyStoriesPrivacy) {
|
|
const footer = (
|
|
<>
|
|
<div />
|
|
<div>
|
|
<Button
|
|
onClick={() => setPage(Page.SendStory)}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('cancel')}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
if (stagedMyStories.isBlockList) {
|
|
if (stagedMyStories.members.length) {
|
|
onHideMyStoriesFrom(stagedMyStoriesMemberUuids);
|
|
} else {
|
|
setMyStoriesToAllSignalConnections();
|
|
}
|
|
} else {
|
|
onViewersUpdated(MY_STORY_ID, stagedMyStoriesMemberUuids);
|
|
}
|
|
|
|
setSelectedContacts([]);
|
|
setPage(Page.SendStory);
|
|
}}
|
|
variant={ButtonVariant.Primary}
|
|
>
|
|
{i18n('save')}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
modal = handleClose => (
|
|
<ModalPage
|
|
modalName="SendStoryModal__my-stories-privacy"
|
|
title={i18n('SendStoryModal__my-stories-privacy')}
|
|
modalFooter={footer}
|
|
onClose={handleClose}
|
|
{...modalCommonProps}
|
|
>
|
|
<EditMyStoryPrivacy
|
|
hasDisclaimerAbove
|
|
i18n={i18n}
|
|
learnMore="SendStoryModal__privacy-disclaimer"
|
|
myStories={stagedMyStories}
|
|
signalConnectionsCount={signalConnections.length}
|
|
onClickExclude={() => {
|
|
let nextSelectedContacts = stagedMyStories.members;
|
|
|
|
if (!stagedMyStories.isBlockList) {
|
|
setStagedMyStories(myStories => ({
|
|
...myStories,
|
|
isBlockList: true,
|
|
members: [],
|
|
}));
|
|
nextSelectedContacts = [];
|
|
}
|
|
|
|
setSelectedContacts(nextSelectedContacts);
|
|
|
|
setPage(Page.HideStoryFrom);
|
|
}}
|
|
onClickOnlyShareWith={() => {
|
|
if (!stagedMyStories.isBlockList) {
|
|
setSelectedContacts(stagedMyStories.members);
|
|
} else {
|
|
setStagedMyStories(myStories => ({
|
|
...myStories,
|
|
isBlockList: false,
|
|
members: [],
|
|
}));
|
|
}
|
|
|
|
setPage(Page.AddViewer);
|
|
}}
|
|
setSelectedContacts={setSelectedContacts}
|
|
setMyStoriesToAllSignalConnections={() => {
|
|
setStagedMyStories(myStories => ({
|
|
...myStories,
|
|
isBlockList: true,
|
|
members: [],
|
|
}));
|
|
setSelectedContacts([]);
|
|
}}
|
|
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
|
/>
|
|
</ModalPage>
|
|
);
|
|
} else if (page === Page.EditingDistributionList && listToEdit) {
|
|
modal = handleClose => (
|
|
<DistributionListSettingsModal
|
|
getPreferredBadge={getPreferredBadge}
|
|
i18n={i18n}
|
|
listToEdit={listToEdit}
|
|
signalConnectionsCount={signalConnections.length}
|
|
onRemoveMembers={onRemoveMembers}
|
|
onRepliesNReactionsChanged={onRepliesNReactionsChanged}
|
|
setConfirmDeleteList={setConfirmDeleteList}
|
|
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
|
setPage={setPage}
|
|
setSelectedContacts={setSelectedContacts}
|
|
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
|
onBackButtonClick={() =>
|
|
confirmDiscardIf(selectedContacts.length > 0, () =>
|
|
setListIdToEdit(undefined)
|
|
)
|
|
}
|
|
onClose={handleClose}
|
|
/>
|
|
);
|
|
} else if (
|
|
page === Page.ChooseViewers ||
|
|
page === Page.NameStory ||
|
|
page === Page.AddViewer ||
|
|
page === Page.HideStoryFrom
|
|
) {
|
|
modal = handleClose => (
|
|
<EditDistributionListModal
|
|
candidateConversations={candidateConversations}
|
|
getPreferredBadge={getPreferredBadge}
|
|
i18n={i18n}
|
|
onCreateList={async (name, uuids) => {
|
|
const newDistributionListId = await onDistributionListCreated(
|
|
name,
|
|
uuids
|
|
);
|
|
|
|
setSelectedContacts([]);
|
|
setSelectedListIds(
|
|
listIds => new Set([...listIds, newDistributionListId])
|
|
);
|
|
|
|
setPage(Page.SendStory);
|
|
}}
|
|
onViewersUpdated={uuids => {
|
|
if (listIdToEdit && page === Page.AddViewer) {
|
|
onViewersUpdated(listIdToEdit, uuids);
|
|
setPage(Page.EditingDistributionList);
|
|
} else if (page === Page.ChooseViewers) {
|
|
setPage(Page.NameStory);
|
|
} else if (listIdToEdit && page === Page.HideStoryFrom) {
|
|
onHideMyStoriesFrom(uuids);
|
|
setPage(Page.SendStory);
|
|
} else if (page === Page.HideStoryFrom || page === Page.AddViewer) {
|
|
setStagedMyStoriesMemberUuids(uuids);
|
|
setPage(Page.SetMyStoriesPrivacy);
|
|
} else {
|
|
setPage(Page.SendStory);
|
|
}
|
|
}}
|
|
page={page}
|
|
onClose={handleClose}
|
|
onBackButtonClick={() =>
|
|
confirmDiscardIf(selectedContacts.length > 0, () => {
|
|
if (listIdToEdit) {
|
|
if (
|
|
page === Page.AddViewer ||
|
|
page === Page.HideStoryFrom ||
|
|
page === Page.ChooseViewers
|
|
) {
|
|
setPage(Page.EditingDistributionList);
|
|
} else {
|
|
setListIdToEdit(undefined);
|
|
}
|
|
} else if (page === Page.HideStoryFrom || page === Page.AddViewer) {
|
|
setSelectedContacts([]);
|
|
setStagedMyStories(initialMyStories);
|
|
setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids);
|
|
setPage(Page.SetMyStoriesPrivacy);
|
|
} else if (page === Page.ChooseViewers) {
|
|
setSelectedContacts([]);
|
|
setPage(Page.SendStory);
|
|
} else if (page === Page.NameStory) {
|
|
setPage(Page.ChooseViewers);
|
|
}
|
|
})
|
|
}
|
|
selectedContacts={selectedContacts}
|
|
setSelectedContacts={setSelectedContacts}
|
|
/>
|
|
);
|
|
} else if (page === Page.ChooseGroups) {
|
|
const footer = (
|
|
<>
|
|
<div className="SendStoryModal__selected-lists">{selectedNames}</div>
|
|
{selectedNames.length > 0 && (
|
|
<button
|
|
aria-label={i18n('ok')}
|
|
className="SendStoryModal__ok"
|
|
disabled={!chosenGroupIds.size}
|
|
onClick={async () => {
|
|
await toggleGroupsForStorySend(Array.from(chosenGroupIds));
|
|
setChosenGroupIds(new Set());
|
|
setSelectedGroupIds(chosenGroupIds);
|
|
setPage(Page.SendStory);
|
|
}}
|
|
type="button"
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
modal = handleClose => (
|
|
<ModalPage
|
|
modalName="SendStoryModal__choose-groups"
|
|
title={i18n('SendStoryModal__choose-groups')}
|
|
moduleClassName="SendStoryModal"
|
|
modalFooter={footer}
|
|
onClose={handleClose}
|
|
{...modalCommonProps}
|
|
>
|
|
<SearchInput
|
|
disabled={groupConversations.length === 0}
|
|
i18n={i18n}
|
|
placeholder={i18n('contactSearchPlaceholder')}
|
|
moduleClassName="StoriesSettingsModal__search"
|
|
onChange={event => {
|
|
setSearchTerm(event.target.value);
|
|
}}
|
|
value={searchTerm}
|
|
/>
|
|
{filteredConversations.length ? (
|
|
filteredConversations.map(group => (
|
|
<Checkbox
|
|
checked={chosenGroupIds.has(group.id)}
|
|
key={group.id}
|
|
label={group.title}
|
|
moduleClassName="SendStoryModal__distribution-list"
|
|
name="SendStoryModal__distribution-list"
|
|
onChange={(value: boolean) => {
|
|
if (group.announcementsOnly && !group.areWeAdmin) {
|
|
setHasAnnouncementsOnlyAlert(true);
|
|
return;
|
|
}
|
|
|
|
setChosenGroupIds(groupIds => {
|
|
if (value) {
|
|
groupIds.add(group.id);
|
|
} else {
|
|
groupIds.delete(group.id);
|
|
}
|
|
return new Set([...groupIds]);
|
|
});
|
|
}}
|
|
>
|
|
{({ id, checkboxNode }) => (
|
|
<>
|
|
<label
|
|
className="SendStoryModal__distribution-list__label"
|
|
htmlFor={id}
|
|
>
|
|
<Avatar
|
|
acceptedMessageRequest={group.acceptedMessageRequest}
|
|
avatarPath={group.avatarPath}
|
|
badge={undefined}
|
|
color={group.color}
|
|
conversationType={group.type}
|
|
i18n={i18n}
|
|
isMe={false}
|
|
sharedGroupNames={[]}
|
|
size={AvatarSize.THIRTY_TWO}
|
|
title={group.title}
|
|
/>
|
|
|
|
<div className="SendStoryModal__distribution-list__info">
|
|
<div className="SendStoryModal__distribution-list__name">
|
|
{group.title}
|
|
</div>
|
|
|
|
<div className="SendStoryModal__distribution-list__description">
|
|
{i18n('icu:ConversationHero--members', {
|
|
count: group.membersCount,
|
|
})}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
{checkboxNode}
|
|
</>
|
|
)}
|
|
</Checkbox>
|
|
))
|
|
) : (
|
|
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
|
{i18n('noGroupsFound')}
|
|
</div>
|
|
)}
|
|
</ModalPage>
|
|
);
|
|
} else {
|
|
const footer = (
|
|
<>
|
|
<div className="SendStoryModal__selected-lists">{selectedNames}</div>
|
|
{selectedNames.length > 0 && (
|
|
<button
|
|
aria-label={i18n('SendStoryModal__send')}
|
|
className="SendStoryModal__send"
|
|
disabled={!selectedListIds.size && !selectedGroupIds.size}
|
|
onClick={() => {
|
|
onSend(Array.from(selectedListIds), Array.from(selectedGroupIds));
|
|
}}
|
|
type="button"
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
const attachment = {
|
|
...draftAttachment,
|
|
url: objectUrl,
|
|
};
|
|
|
|
// my stories always first, the rest sorted by recency
|
|
const fullList = sortBy(
|
|
[...groupStories, ...distributionLists],
|
|
listOrGroup => {
|
|
if (listOrGroup.id === MY_STORY_ID) {
|
|
return Number.NEGATIVE_INFINITY;
|
|
}
|
|
return (
|
|
(mostRecentActiveStoryTimestampByGroupOrDistributionList[
|
|
listOrGroup.id
|
|
] ?? 0) * -1
|
|
);
|
|
}
|
|
);
|
|
|
|
const renderDistributionList = (
|
|
list: StoryDistributionListWithMembersDataType
|
|
): JSX.Element => {
|
|
return (
|
|
<Checkbox
|
|
checked={selectedListIds.has(list.id)}
|
|
key={list.id}
|
|
label={getStoryDistributionListName(i18n, list.id, list.name)}
|
|
moduleClassName="SendStoryModal__distribution-list"
|
|
name="SendStoryModal__distribution-list"
|
|
onChange={(value: boolean) => {
|
|
if (
|
|
list.id === MY_STORY_ID &&
|
|
hasFirstStoryPostExperience &&
|
|
value
|
|
) {
|
|
setPage(Page.SetMyStoriesPrivacy);
|
|
return;
|
|
}
|
|
|
|
setSelectedListIds(listIds => {
|
|
if (value) {
|
|
listIds.add(list.id);
|
|
} else {
|
|
listIds.delete(list.id);
|
|
}
|
|
return new Set([...listIds]);
|
|
});
|
|
if (value && ourConversationId) {
|
|
onSelectedStoryList({
|
|
conversationId: ourConversationId,
|
|
distributionId: list.id,
|
|
uuids: getListMemberUuids(list, signalConnections),
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{({ id, checkboxNode }) => (
|
|
<ContextMenu
|
|
i18n={i18n}
|
|
menuOptions={
|
|
list.id === MY_STORY_ID
|
|
? [
|
|
{
|
|
label: i18n('StoriesSettings__context-menu'),
|
|
icon: 'SendStoryModal__icon--delete',
|
|
onClick: () => setListIdToEdit(list.id),
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
label: i18n('StoriesSettings__context-menu'),
|
|
icon: 'SendStoryModal__icon--settings',
|
|
onClick: () => setListIdToEdit(list.id),
|
|
},
|
|
{
|
|
label: i18n('SendStoryModal__delete-story'),
|
|
icon: 'SendStoryModal__icon--delete',
|
|
onClick: () => setConfirmDeleteList(list),
|
|
},
|
|
]
|
|
}
|
|
moduleClassName="SendStoryModal__distribution-list-context"
|
|
onClick={noop}
|
|
popperOptions={{
|
|
placement: 'bottom',
|
|
strategy: 'absolute',
|
|
}}
|
|
theme={Theme.Dark}
|
|
>
|
|
<label
|
|
className="SendStoryModal__distribution-list__label"
|
|
htmlFor={id}
|
|
>
|
|
{list.id === MY_STORY_ID ? (
|
|
<Avatar
|
|
acceptedMessageRequest={me.acceptedMessageRequest}
|
|
avatarPath={me.avatarPath}
|
|
badge={undefined}
|
|
color={me.color}
|
|
conversationType={me.type}
|
|
i18n={i18n}
|
|
isMe
|
|
sharedGroupNames={me.sharedGroupNames}
|
|
size={AvatarSize.THIRTY_TWO}
|
|
title={me.title}
|
|
/>
|
|
) : (
|
|
<span className="StoriesSettingsModal__list__avatar--custom" />
|
|
)}
|
|
|
|
<div className="SendStoryModal__distribution-list__info">
|
|
<div className="SendStoryModal__distribution-list__name">
|
|
<StoryDistributionListName
|
|
i18n={i18n}
|
|
id={list.id}
|
|
name={list.name}
|
|
/>
|
|
</div>
|
|
|
|
<div className="SendStoryModal__distribution-list__description">
|
|
{hasFirstStoryPostExperience && list.id === MY_STORY_ID ? (
|
|
i18n('SendStoryModal__choose-who-can-view')
|
|
) : (
|
|
<>
|
|
<span className="SendStoryModal__rtl-span">
|
|
{list.id === MY_STORY_ID
|
|
? getI18nForMyStory(list, i18n)
|
|
: i18n('SendStoryModal__custom-story')}
|
|
</span>
|
|
<span className="SendStoryModal__rtl-span">
|
|
·
|
|
</span>
|
|
<span className="SendStoryModal__rtl-span">
|
|
{list.isBlockList && list.members.length > 0
|
|
? i18n('icu:SendStoryModal__excluded', {
|
|
count: list.members.length,
|
|
})
|
|
: getListViewers(list, i18n, signalConnections)}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
{checkboxNode}
|
|
</ContextMenu>
|
|
)}
|
|
</Checkbox>
|
|
);
|
|
};
|
|
|
|
const renderGroup = (group: ConversationType) => {
|
|
return (
|
|
<Checkbox
|
|
checked={selectedGroupIds.has(group.id)}
|
|
key={group.id}
|
|
label={group.title}
|
|
moduleClassName="SendStoryModal__distribution-list"
|
|
name="SendStoryModal__distribution-list"
|
|
onChange={(value: boolean) => {
|
|
if (!group.memberships) {
|
|
return;
|
|
}
|
|
|
|
if (group.announcementsOnly && !group.areWeAdmin) {
|
|
setHasAnnouncementsOnlyAlert(true);
|
|
return;
|
|
}
|
|
|
|
setSelectedGroupIds(groupIds => {
|
|
if (value) {
|
|
groupIds.add(group.id);
|
|
} else {
|
|
groupIds.delete(group.id);
|
|
}
|
|
return new Set([...groupIds]);
|
|
});
|
|
if (value) {
|
|
onSelectedStoryList({
|
|
conversationId: group.id,
|
|
distributionId: undefined,
|
|
uuids: group.memberships.map(({ uuid }) => uuid),
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{({ id, checkboxNode }) => (
|
|
<ContextMenu
|
|
i18n={i18n}
|
|
menuOptions={[
|
|
{
|
|
label: i18n('SendStoryModal__delete-story'),
|
|
icon: 'SendStoryModal__icon--delete',
|
|
onClick: () => setConfirmRemoveGroupId(group.id),
|
|
},
|
|
]}
|
|
moduleClassName="SendStoryModal__distribution-list-context"
|
|
onClick={noop}
|
|
popperOptions={{
|
|
placement: 'bottom',
|
|
strategy: 'absolute',
|
|
}}
|
|
theme={Theme.Dark}
|
|
>
|
|
<label
|
|
className="SendStoryModal__distribution-list__label"
|
|
htmlFor={id}
|
|
>
|
|
<Avatar
|
|
acceptedMessageRequest={group.acceptedMessageRequest}
|
|
avatarPath={group.avatarPath}
|
|
badge={undefined}
|
|
color={group.color}
|
|
conversationType={group.type}
|
|
i18n={i18n}
|
|
isMe={false}
|
|
sharedGroupNames={[]}
|
|
size={AvatarSize.THIRTY_TWO}
|
|
title={group.title}
|
|
/>
|
|
|
|
<div className="SendStoryModal__distribution-list__info">
|
|
<div className="SendStoryModal__distribution-list__name">
|
|
{group.title}
|
|
</div>
|
|
|
|
<div className="SendStoryModal__distribution-list__description">
|
|
<span className="SendStoryModal__rtl-span">
|
|
{i18n('SendStoryModal__group-story')}
|
|
</span>
|
|
<span className="SendStoryModal__rtl-span">
|
|
·
|
|
</span>
|
|
<span className="SendStoryModal__rtl-span">
|
|
{i18n('icu:ConversationHero--members', {
|
|
count: group.membersCount,
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
{checkboxNode}
|
|
</ContextMenu>
|
|
)}
|
|
</Checkbox>
|
|
);
|
|
};
|
|
|
|
modal = handleClose => (
|
|
<ModalPage
|
|
modalName="SendStoryModal__title"
|
|
title={i18n('SendStoryModal__title')}
|
|
moduleClassName="SendStoryModal"
|
|
modalFooter={footer}
|
|
onClose={handleClose}
|
|
{...modalCommonProps}
|
|
>
|
|
<div
|
|
className="SendStoryModal__story-preview"
|
|
style={{ backgroundImage: getStoryBackground(attachment) }}
|
|
>
|
|
<StoryImage
|
|
i18n={i18n}
|
|
firstName={i18n('you')}
|
|
queueStoryDownload={noop}
|
|
storyId="story-id"
|
|
label="label"
|
|
moduleClassName="SendStoryModal__story"
|
|
attachment={attachment}
|
|
/>
|
|
</div>
|
|
<div className="SendStoryModal__top-bar">
|
|
{i18n('stories')}
|
|
<ContextMenu
|
|
aria-label={i18n('SendStoryModal__new')}
|
|
i18n={i18n}
|
|
menuOptions={[
|
|
{
|
|
label: i18n('SendStoryModal__new-custom--title'),
|
|
description: i18n('SendStoryModal__new-custom--description'),
|
|
icon: 'SendStoryModal__icon--custom',
|
|
onClick: () => setPage(Page.ChooseViewers),
|
|
},
|
|
{
|
|
label: i18n('SendStoryModal__new-group--title'),
|
|
description: i18n('SendStoryModal__new-group--description'),
|
|
icon: 'SendStoryModal__icon--group',
|
|
onClick: () => setPage(Page.ChooseGroups),
|
|
},
|
|
]}
|
|
moduleClassName="SendStoryModal__new-story"
|
|
popperOptions={{
|
|
placement: 'bottom',
|
|
strategy: 'absolute',
|
|
}}
|
|
theme={Theme.Dark}
|
|
>
|
|
{({ openMenu, onKeyDown, ref, menuNode }) => (
|
|
<div>
|
|
<Button
|
|
ref={ref}
|
|
className="SendStoryModal__new-story__button"
|
|
variant={ButtonVariant.Secondary}
|
|
size={ButtonSize.Small}
|
|
onClick={openMenu}
|
|
onKeyDown={onKeyDown}
|
|
>
|
|
{i18n('SendStoryModal__new')}
|
|
</Button>
|
|
{menuNode}
|
|
</div>
|
|
)}
|
|
</ContextMenu>
|
|
</div>
|
|
{fullList.map(listOrGroup =>
|
|
// only group has a type field
|
|
'type' in listOrGroup
|
|
? renderGroup(listOrGroup)
|
|
: renderDistributionList(listOrGroup)
|
|
)}
|
|
</ModalPage>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{!confirmDiscardModal && (
|
|
<PagedModal
|
|
modalName="SendStoryModal"
|
|
theme={Theme.Dark}
|
|
onClose={() => confirmDiscardIf(selectedContacts.length > 0, onClose)}
|
|
>
|
|
{modal}
|
|
</PagedModal>
|
|
)}
|
|
{hasAnnouncementsOnlyAlert && (
|
|
<Alert
|
|
body={i18n('SendStoryModal__announcements-only')}
|
|
i18n={i18n}
|
|
onClose={() => setHasAnnouncementsOnlyAlert(false)}
|
|
theme={Theme.Dark}
|
|
/>
|
|
)}
|
|
{confirmRemoveGroupId && (
|
|
<ConfirmationDialog
|
|
dialogName="SendStoryModal.confirmRemoveGroupId"
|
|
actions={[
|
|
{
|
|
action: () => {
|
|
void toggleGroupsForStorySend([confirmRemoveGroupId]);
|
|
setConfirmRemoveGroupId(undefined);
|
|
},
|
|
style: 'negative',
|
|
text: i18n('delete'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmRemoveGroupId(undefined);
|
|
}}
|
|
theme={Theme.Dark}
|
|
>
|
|
{i18n('SendStoryModal__confirm-remove-group')}
|
|
</ConfirmationDialog>
|
|
)}
|
|
{confirmDeleteList && (
|
|
<ConfirmationDialog
|
|
dialogName="SendStoryModal.confirmDeleteList"
|
|
actions={[
|
|
{
|
|
action: () => {
|
|
onDeleteList(confirmDeleteList.id);
|
|
setConfirmDeleteList(undefined);
|
|
},
|
|
style: 'negative',
|
|
text: i18n('delete'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmDeleteList(undefined);
|
|
}}
|
|
theme={Theme.Dark}
|
|
>
|
|
{i18n('StoriesSettings__delete-list--confirm', [
|
|
confirmDeleteList.name,
|
|
])}
|
|
</ConfirmationDialog>
|
|
)}
|
|
{confirmDiscardModal}
|
|
</>
|
|
);
|
|
}
|