Show and sort active groups when sending a story
This commit is contained in:
parent
5078bc83f4
commit
d0fb25f758
9 changed files with 336 additions and 228 deletions
|
@ -5779,6 +5779,22 @@
|
||||||
"message": "Only admins can send stories to this group.",
|
"message": "Only admins can send stories to this group.",
|
||||||
"description": "Alert body for groups that non-admins cannot send stories to"
|
"description": "Alert body for groups that non-admins cannot send stories to"
|
||||||
},
|
},
|
||||||
|
"icu:SendStoryModal__my-stories-description-all": {
|
||||||
|
"messageformat": "All Signal connections · {viewersCount, plural, one {1 viewer} other {# viewers}}",
|
||||||
|
"description": "Shown as a subtitle under My Stories option in the send-story-to dialog when not exluding anyone"
|
||||||
|
},
|
||||||
|
"icu:SendStoryModal__my-stories-description-excluding": {
|
||||||
|
"messageformat": "All Signal connections · {excludedCount, plural, one {1 excluded} other {# excluded}}",
|
||||||
|
"description": "Shown as a subtitle under My Stories option in the send-story-to dialog when excluding some"
|
||||||
|
},
|
||||||
|
"icu:SendStoryModal__private-story-description": {
|
||||||
|
"messageformat": "Private story · {viewersCount, plural, one {1 viewer} other {# viewers}}",
|
||||||
|
"description": "Shown as a subtitle of each private story in the send-story-to dialog"
|
||||||
|
},
|
||||||
|
"icu:SendStoryModal__group-story-description": {
|
||||||
|
"messageformat": "Group story · {membersCount, plural, one {1 member} other {# members}}",
|
||||||
|
"description": "Shown as a subtitle of each group story in the send-story-to dialog"
|
||||||
|
},
|
||||||
"Stories__settings-toggle--title": {
|
"Stories__settings-toggle--title": {
|
||||||
"message": "Share & View Stories",
|
"message": "Share & View Stories",
|
||||||
"description": "Select box title for the stories on/off toggle"
|
"description": "Select box title for the stories on/off toggle"
|
||||||
|
|
|
@ -136,6 +136,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__distribution-list {
|
&__distribution-list {
|
||||||
|
height: 52px;
|
||||||
&__container {
|
&__container {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { noop } from 'lodash';
|
import { noop, sortBy } from 'lodash';
|
||||||
|
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||||
|
@ -62,6 +62,10 @@ export type PropsType = {
|
||||||
) => unknown;
|
) => unknown;
|
||||||
signalConnections: Array<ConversationType>;
|
signalConnections: Array<ConversationType>;
|
||||||
toggleGroupsForStorySend: (cids: Array<string>) => unknown;
|
toggleGroupsForStorySend: (cids: Array<string>) => unknown;
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList: Record<
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
>;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
StoriesSettingsModalPropsType,
|
StoriesSettingsModalPropsType,
|
||||||
| 'onHideMyStoriesFrom'
|
| 'onHideMyStoriesFrom'
|
||||||
|
@ -137,6 +141,7 @@ export const SendStoryModal = ({
|
||||||
setMyStoriesToAllSignalConnections,
|
setMyStoriesToAllSignalConnections,
|
||||||
signalConnections,
|
signalConnections,
|
||||||
toggleGroupsForStorySend,
|
toggleGroupsForStorySend,
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||||
toggleSignalConnectionsModal,
|
toggleSignalConnectionsModal,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [page, setPage] = useState<PageType>(Page.SendStory);
|
const [page, setPage] = useState<PageType>(Page.SendStory);
|
||||||
|
@ -240,7 +245,7 @@ export const SendStoryModal = ({
|
||||||
[distributionLists]
|
[distributionLists]
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialMyStories = useMemo(
|
const initialMyStories: StoryDistributionListWithMembersDataType = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
allowsReplies: true,
|
allowsReplies: true,
|
||||||
id: MY_STORIES_ID,
|
id: MY_STORIES_ID,
|
||||||
|
@ -597,6 +602,243 @@ export const SendStoryModal = ({
|
||||||
url: objectUrl,
|
url: objectUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// my stories always first, the rest sorted by recency
|
||||||
|
const fullList = sortBy(
|
||||||
|
[...groupStories, ...distributionLists],
|
||||||
|
listOrGroup => {
|
||||||
|
if (listOrGroup.id === MY_STORIES_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_STORIES_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) {
|
||||||
|
onSelectedStoryList(getListMemberUuids(list, signalConnections));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ id, checkboxNode }) => (
|
||||||
|
<ContextMenu
|
||||||
|
i18n={i18n}
|
||||||
|
menuOptions={
|
||||||
|
list.id === MY_STORIES_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_STORIES_ID ? (
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={me.acceptedMessageRequest}
|
||||||
|
avatarPath={me.avatarPath}
|
||||||
|
badge={undefined}
|
||||||
|
color={me.color}
|
||||||
|
conversationType={me.type}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe
|
||||||
|
sharedGroupNames={me.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_SIX}
|
||||||
|
title={me.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="StoriesSettingsModal__list__avatar--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_STORIES_ID ? (
|
||||||
|
i18n('SendStoryModal__choose-who-can-view')
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="SendStoryModal__rtl-span">
|
||||||
|
{list.id === MY_STORIES_ID
|
||||||
|
? i18n(getKeyForMyStoryType(list))
|
||||||
|
: 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(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_SIX}
|
||||||
|
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 => (
|
modal = handleClose => (
|
||||||
<ModalPage
|
<ModalPage
|
||||||
modalName="SendStoryModal__title"
|
modalName="SendStoryModal__title"
|
||||||
|
@ -663,222 +905,12 @@ export const SendStoryModal = ({
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
{distributionLists.map(list => (
|
{fullList.map(listOrGroup =>
|
||||||
<Checkbox
|
// only group has a type field
|
||||||
checked={selectedListIds.has(list.id)}
|
'type' in listOrGroup
|
||||||
key={list.id}
|
? renderGroup(listOrGroup)
|
||||||
label={getStoryDistributionListName(i18n, list.id, list.name)}
|
: renderDistributionList(listOrGroup)
|
||||||
moduleClassName="SendStoryModal__distribution-list"
|
)}
|
||||||
name="SendStoryModal__distribution-list"
|
|
||||||
onChange={(value: boolean) => {
|
|
||||||
if (
|
|
||||||
list.id === MY_STORIES_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) {
|
|
||||||
onSelectedStoryList(
|
|
||||||
getListMemberUuids(list, signalConnections)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ id, checkboxNode }) => (
|
|
||||||
<ContextMenu
|
|
||||||
i18n={i18n}
|
|
||||||
menuOptions={
|
|
||||||
list.id === MY_STORIES_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_STORIES_ID ? (
|
|
||||||
<Avatar
|
|
||||||
acceptedMessageRequest={me.acceptedMessageRequest}
|
|
||||||
avatarPath={me.avatarPath}
|
|
||||||
badge={undefined}
|
|
||||||
color={me.color}
|
|
||||||
conversationType={me.type}
|
|
||||||
i18n={i18n}
|
|
||||||
isMe
|
|
||||||
sharedGroupNames={me.sharedGroupNames}
|
|
||||||
size={AvatarSize.THIRTY_SIX}
|
|
||||||
title={me.title}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="StoriesSettingsModal__list__avatar--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_STORIES_ID ? (
|
|
||||||
i18n('SendStoryModal__choose-who-can-view')
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="SendStoryModal__rtl-span">
|
|
||||||
{list.id === MY_STORIES_ID
|
|
||||||
? i18n(getKeyForMyStoryType(list))
|
|
||||||
: 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>
|
|
||||||
))}
|
|
||||||
{groupStories.map(group => (
|
|
||||||
<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(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_SIX}
|
|
||||||
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>
|
|
||||||
))}
|
|
||||||
</ModalPage>
|
</ModalPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ export type PropsType = {
|
||||||
| 'setMyStoriesToAllSignalConnections'
|
| 'setMyStoriesToAllSignalConnections'
|
||||||
| 'signalConnections'
|
| 'signalConnections'
|
||||||
| 'toggleGroupsForStorySend'
|
| 'toggleGroupsForStorySend'
|
||||||
|
| 'mostRecentActiveStoryTimestampByGroupOrDistributionList'
|
||||||
| 'toggleSignalConnectionsModal'
|
| 'toggleSignalConnectionsModal'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -98,6 +99,7 @@ export const StoryCreator = ({
|
||||||
setMyStoriesToAllSignalConnections,
|
setMyStoriesToAllSignalConnections,
|
||||||
signalConnections,
|
signalConnections,
|
||||||
toggleGroupsForStorySend,
|
toggleGroupsForStorySend,
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||||
toggleSignalConnectionsModal,
|
toggleSignalConnectionsModal,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [draftAttachment, setDraftAttachment] = useState<
|
const [draftAttachment, setDraftAttachment] = useState<
|
||||||
|
@ -171,6 +173,9 @@ export const StoryCreator = ({
|
||||||
}
|
}
|
||||||
signalConnections={signalConnections}
|
signalConnections={signalConnections}
|
||||||
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList={
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||||
|
}
|
||||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5526,6 +5526,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return window.textsecure.storage.protocol.signAlternateIdentity();
|
return window.textsecure.storage.protocol.signAlternateIdentity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return only undefined if not a group */
|
||||||
getStorySendMode(): StorySendMode | undefined {
|
getStorySendMode(): StorySendMode | undefined {
|
||||||
if (!isGroup(this.attributes)) {
|
if (!isGroup(this.attributes)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -916,7 +916,7 @@ async function getAvatarsAndUpdateConversation(
|
||||||
conversation.attributes.avatars = nextAvatars.map(avatarData =>
|
conversation.attributes.avatars = nextAvatars.map(avatarData =>
|
||||||
omit(avatarData, ['buffer'])
|
omit(avatarData, ['buffer'])
|
||||||
);
|
);
|
||||||
await window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
|
||||||
return nextAvatars;
|
return nextAvatars;
|
||||||
}
|
}
|
||||||
|
@ -1264,7 +1264,7 @@ export function setVoiceNotePlaybackRate({
|
||||||
} else {
|
} else {
|
||||||
conversationModel.attributes.voiceNotePlaybackRate = rate;
|
conversationModel.attributes.voiceNotePlaybackRate = rate;
|
||||||
}
|
}
|
||||||
await window.Signal.Data.updateConversation(conversationModel.attributes);
|
window.Signal.Data.updateConversation(conversationModel.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = conversationModel?.format();
|
const conversation = conversationModel?.format();
|
||||||
|
@ -1314,7 +1314,7 @@ function colorSelected({
|
||||||
delete conversation.attributes.customColorId;
|
delete conversation.attributes.customColorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -2016,7 +2016,7 @@ function toggleGroupsForStorySend(
|
||||||
conversation.set({
|
conversation.set({
|
||||||
storySendMode: newStorySendMode,
|
storySendMode: newStorySendMode,
|
||||||
});
|
});
|
||||||
await window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
conversation.captureChange('storySendMode');
|
conversation.captureChange('storySendMode');
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
||||||
MessagesByConversationType,
|
MessagesByConversationType,
|
||||||
PreJoinConversationType,
|
PreJoinConversationType,
|
||||||
} from '../ducks/conversations';
|
} from '../ducks/conversations';
|
||||||
import type { StoriesStateType } from '../ducks/stories';
|
import type { StoriesStateType, StoryDataType } from '../ducks/stories';
|
||||||
import {
|
import {
|
||||||
ComposerStep,
|
ComposerStep,
|
||||||
OneTimeModalState,
|
OneTimeModalState,
|
||||||
|
@ -61,6 +61,7 @@ import type { AccountSelectorType } from './accounts';
|
||||||
import { getAccountSelector } from './accounts';
|
import { getAccountSelector } from './accounts';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
import { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
||||||
|
import { reduce } from '../../util/iterables';
|
||||||
|
|
||||||
let placeholderContact: ConversationType;
|
let placeholderContact: ConversationType;
|
||||||
export const getPlaceholderContact = (): ConversationType => {
|
export const getPlaceholderContact = (): ConversationType => {
|
||||||
|
@ -545,6 +546,29 @@ export const getNonGroupStories = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const selectMostRecentActiveStoryTimestampByGroupOrDistributionList =
|
||||||
|
createSelector(
|
||||||
|
(state: StateType): Array<StoryDataType> => state.stories.stories,
|
||||||
|
(stories: Array<StoryDataType>): Record<string, number> => {
|
||||||
|
return reduce<StoryDataType, Record<string, number>>(
|
||||||
|
stories,
|
||||||
|
(acc, story) => {
|
||||||
|
const distributionListOrConversationId =
|
||||||
|
story.storyDistributionListId ?? story.conversationId;
|
||||||
|
const cur = acc[distributionListOrConversationId];
|
||||||
|
if (cur && story.timestamp < cur) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[distributionListOrConversationId]: story.timestamp,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getGroupStories = createSelector(
|
export const getGroupStories = createSelector(
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
getConversationIdsWithStories,
|
getConversationIdsWithStories,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
getGroupStories,
|
getGroupStories,
|
||||||
getMe,
|
getMe,
|
||||||
getNonGroupStories,
|
getNonGroupStories,
|
||||||
|
selectMostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
|
import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
@ -70,6 +71,9 @@ export function SmartStoryCreator(): JSX.Element | null {
|
||||||
const me = useSelector(getMe);
|
const me = useSelector(getMe);
|
||||||
const recentStickers = useSelector(getRecentStickers);
|
const recentStickers = useSelector(getRecentStickers);
|
||||||
const signalConnections = useSelector(getAllSignalConnections);
|
const signalConnections = useSelector(getAllSignalConnections);
|
||||||
|
const mostRecentActiveStoryTimestampByGroupOrDistributionList = useSelector(
|
||||||
|
selectMostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||||
|
);
|
||||||
|
|
||||||
const addStoryData = useSelector(getAddStoryData);
|
const addStoryData = useSelector(getAddStoryData);
|
||||||
const file = addStoryData?.type === 'Media' ? addStoryData.file : undefined;
|
const file = addStoryData?.type === 'Media' ? addStoryData.file : undefined;
|
||||||
|
@ -106,6 +110,9 @@ export function SmartStoryCreator(): JSX.Element | null {
|
||||||
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
||||||
signalConnections={signalConnections}
|
signalConnections={signalConnections}
|
||||||
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList={
|
||||||
|
mostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||||
|
}
|
||||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { UUIDStringType } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
import { DAY, SECOND } from './durations';
|
import { DAY, SECOND } from './durations';
|
||||||
import { MY_STORIES_ID } from '../types/Stories';
|
import { MY_STORIES_ID, StorySendMode } from '../types/Stories';
|
||||||
import { getStoriesBlocked } from './stories';
|
import { getStoriesBlocked } from './stories';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import { SeenStatus } from '../MessageSeenStatus';
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
|
@ -23,6 +23,7 @@ import { getSignalConnections } from './getSignalConnections';
|
||||||
import { incrementMessageCounter } from './incrementMessageCounter';
|
import { incrementMessageCounter } from './incrementMessageCounter';
|
||||||
import { isGroupV2 } from './whatTypeOfConversation';
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
import { isNotNil } from './isNotNil';
|
import { isNotNil } from './isNotNil';
|
||||||
|
import { collect } from './iterables';
|
||||||
|
|
||||||
export async function sendStoryMessage(
|
export async function sendStoryMessage(
|
||||||
listIds: Array<string>,
|
listIds: Array<string>,
|
||||||
|
@ -180,8 +181,8 @@ export async function sendStoryMessage(
|
||||||
MessageAttributesType
|
MessageAttributesType
|
||||||
>();
|
>();
|
||||||
|
|
||||||
await Promise.all(
|
const groupsToSendTo = Array.from(
|
||||||
conversationIds.map(async (conversationId, index) => {
|
collect(conversationIds, conversationId => {
|
||||||
const group = window.ConversationController.get(conversationId);
|
const group = window.ConversationController.get(conversationId);
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
|
@ -208,6 +209,27 @@ export async function sendStoryMessage(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// sending a story to a group marks it as one we want to always
|
||||||
|
// include on the send-story-to list
|
||||||
|
const groupsToUpdate = Array.from(groupsToSendTo).filter(
|
||||||
|
group => group.getStorySendMode() !== StorySendMode.Always
|
||||||
|
);
|
||||||
|
for (const group of groupsToUpdate) {
|
||||||
|
group.set('storySendMode', StorySendMode.Always);
|
||||||
|
}
|
||||||
|
window.Signal.Data.updateConversations(
|
||||||
|
groupsToUpdate.map(group => group.attributes)
|
||||||
|
);
|
||||||
|
for (const group of groupsToUpdate) {
|
||||||
|
group.captureChange('storySendMode');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
groupsToSendTo.map(async (group, index) => {
|
||||||
// We want all of these timestamps to be different from the My Story timestamp.
|
// We want all of these timestamps to be different from the My Story timestamp.
|
||||||
const groupTimestamp = timestamp + index + 1;
|
const groupTimestamp = timestamp + index + 1;
|
||||||
|
|
||||||
|
@ -238,7 +260,7 @@ export async function sendStoryMessage(
|
||||||
await window.Signal.Migrations.upgradeMessageSchema({
|
await window.Signal.Migrations.upgradeMessageSchema({
|
||||||
attachments,
|
attachments,
|
||||||
canReplyToStory: true,
|
canReplyToStory: true,
|
||||||
conversationId,
|
conversationId: group.id,
|
||||||
expireTimer: DAY / SECOND,
|
expireTimer: DAY / SECOND,
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
id: UUID.generate().toString(),
|
id: UUID.generate().toString(),
|
||||||
|
@ -255,7 +277,7 @@ export async function sendStoryMessage(
|
||||||
type: 'story',
|
type: 'story',
|
||||||
});
|
});
|
||||||
|
|
||||||
groupV2MessagesByConversationId.set(conversationId, messageAttributes);
|
groupV2MessagesByConversationId.set(group.id, messageAttributes);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue