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.",
|
||||
"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": {
|
||||
"message": "Share & View Stories",
|
||||
"description": "Select box title for the stories on/off toggle"
|
||||
|
|
|
@ -136,6 +136,7 @@
|
|||
}
|
||||
|
||||
&__distribution-list {
|
||||
height: 52px;
|
||||
&__container {
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { noop, sortBy } from 'lodash';
|
||||
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
|
@ -62,6 +62,10 @@ export type PropsType = {
|
|||
) => unknown;
|
||||
signalConnections: Array<ConversationType>;
|
||||
toggleGroupsForStorySend: (cids: Array<string>) => unknown;
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList: Record<
|
||||
string,
|
||||
number
|
||||
>;
|
||||
} & Pick<
|
||||
StoriesSettingsModalPropsType,
|
||||
| 'onHideMyStoriesFrom'
|
||||
|
@ -137,6 +141,7 @@ export const SendStoryModal = ({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
toggleGroupsForStorySend,
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||
toggleSignalConnectionsModal,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [page, setPage] = useState<PageType>(Page.SendStory);
|
||||
|
@ -240,7 +245,7 @@ export const SendStoryModal = ({
|
|||
[distributionLists]
|
||||
);
|
||||
|
||||
const initialMyStories = useMemo(
|
||||
const initialMyStories: StoryDistributionListWithMembersDataType = useMemo(
|
||||
() => ({
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
|
@ -597,6 +602,243 @@ export const SendStoryModal = ({
|
|||
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 => (
|
||||
<ModalPage
|
||||
modalName="SendStoryModal__title"
|
||||
|
@ -663,222 +905,12 @@ export const SendStoryModal = ({
|
|||
)}
|
||||
</ContextMenu>
|
||||
</div>
|
||||
{distributionLists.map(list => (
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
))}
|
||||
{fullList.map(listOrGroup =>
|
||||
// only group has a type field
|
||||
'type' in listOrGroup
|
||||
? renderGroup(listOrGroup)
|
||||
: renderDistributionList(listOrGroup)
|
||||
)}
|
||||
</ModalPage>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ export type PropsType = {
|
|||
| 'setMyStoriesToAllSignalConnections'
|
||||
| 'signalConnections'
|
||||
| 'toggleGroupsForStorySend'
|
||||
| 'mostRecentActiveStoryTimestampByGroupOrDistributionList'
|
||||
| 'toggleSignalConnectionsModal'
|
||||
>;
|
||||
|
||||
|
@ -98,6 +99,7 @@ export const StoryCreator = ({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
toggleGroupsForStorySend,
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||
toggleSignalConnectionsModal,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [draftAttachment, setDraftAttachment] = useState<
|
||||
|
@ -171,6 +173,9 @@ export const StoryCreator = ({
|
|||
}
|
||||
signalConnections={signalConnections}
|
||||
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList={
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||
}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -5526,6 +5526,7 @@ export class ConversationModel extends window.Backbone
|
|||
return window.textsecure.storage.protocol.signAlternateIdentity();
|
||||
}
|
||||
|
||||
/** @return only undefined if not a group */
|
||||
getStorySendMode(): StorySendMode | undefined {
|
||||
if (!isGroup(this.attributes)) {
|
||||
return undefined;
|
||||
|
|
|
@ -916,7 +916,7 @@ async function getAvatarsAndUpdateConversation(
|
|||
conversation.attributes.avatars = nextAvatars.map(avatarData =>
|
||||
omit(avatarData, ['buffer'])
|
||||
);
|
||||
await window.Signal.Data.updateConversation(conversation.attributes);
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
|
||||
return nextAvatars;
|
||||
}
|
||||
|
@ -1264,7 +1264,7 @@ export function setVoiceNotePlaybackRate({
|
|||
} else {
|
||||
conversationModel.attributes.voiceNotePlaybackRate = rate;
|
||||
}
|
||||
await window.Signal.Data.updateConversation(conversationModel.attributes);
|
||||
window.Signal.Data.updateConversation(conversationModel.attributes);
|
||||
}
|
||||
|
||||
const conversation = conversationModel?.format();
|
||||
|
@ -1314,7 +1314,7 @@ function colorSelected({
|
|||
delete conversation.attributes.customColorId;
|
||||
}
|
||||
|
||||
await window.Signal.Data.updateConversation(conversation.attributes);
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
|
@ -2016,7 +2016,7 @@ function toggleGroupsForStorySend(
|
|||
conversation.set({
|
||||
storySendMode: newStorySendMode,
|
||||
});
|
||||
await window.Signal.Data.updateConversation(conversation.attributes);
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
conversation.captureChange('storySendMode');
|
||||
})
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
|||
MessagesByConversationType,
|
||||
PreJoinConversationType,
|
||||
} from '../ducks/conversations';
|
||||
import type { StoriesStateType } from '../ducks/stories';
|
||||
import type { StoriesStateType, StoryDataType } from '../ducks/stories';
|
||||
import {
|
||||
ComposerStep,
|
||||
OneTimeModalState,
|
||||
|
@ -61,6 +61,7 @@ import type { AccountSelectorType } from './accounts';
|
|||
import { getAccountSelector } from './accounts';
|
||||
import * as log from '../../logging/log';
|
||||
import { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
||||
import { reduce } from '../../util/iterables';
|
||||
|
||||
let placeholderContact: 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(
|
||||
getConversationLookup,
|
||||
getConversationIdsWithStories,
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
getGroupStories,
|
||||
getMe,
|
||||
getNonGroupStories,
|
||||
selectMostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||
} from '../selectors/conversations';
|
||||
import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
@ -70,6 +71,9 @@ export function SmartStoryCreator(): JSX.Element | null {
|
|||
const me = useSelector(getMe);
|
||||
const recentStickers = useSelector(getRecentStickers);
|
||||
const signalConnections = useSelector(getAllSignalConnections);
|
||||
const mostRecentActiveStoryTimestampByGroupOrDistributionList = useSelector(
|
||||
selectMostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||
);
|
||||
|
||||
const addStoryData = useSelector(getAddStoryData);
|
||||
const file = addStoryData?.type === 'Media' ? addStoryData.file : undefined;
|
||||
|
@ -106,6 +110,9 @@ export function SmartStoryCreator(): JSX.Element | null {
|
|||
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
||||
signalConnections={signalConnections}
|
||||
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList={
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||
}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { UUIDStringType } from '../types/UUID';
|
|||
import * as log from '../logging/log';
|
||||
import dataInterface from '../sql/Client';
|
||||
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 { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { SeenStatus } from '../MessageSeenStatus';
|
||||
|
@ -23,6 +23,7 @@ import { getSignalConnections } from './getSignalConnections';
|
|||
import { incrementMessageCounter } from './incrementMessageCounter';
|
||||
import { isGroupV2 } from './whatTypeOfConversation';
|
||||
import { isNotNil } from './isNotNil';
|
||||
import { collect } from './iterables';
|
||||
|
||||
export async function sendStoryMessage(
|
||||
listIds: Array<string>,
|
||||
|
@ -180,8 +181,8 @@ export async function sendStoryMessage(
|
|||
MessageAttributesType
|
||||
>();
|
||||
|
||||
await Promise.all(
|
||||
conversationIds.map(async (conversationId, index) => {
|
||||
const groupsToSendTo = Array.from(
|
||||
collect(conversationIds, conversationId => {
|
||||
const group = window.ConversationController.get(conversationId);
|
||||
|
||||
if (!group) {
|
||||
|
@ -208,6 +209,27 @@ export async function sendStoryMessage(
|
|||
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.
|
||||
const groupTimestamp = timestamp + index + 1;
|
||||
|
||||
|
@ -238,7 +260,7 @@ export async function sendStoryMessage(
|
|||
await window.Signal.Migrations.upgradeMessageSchema({
|
||||
attachments,
|
||||
canReplyToStory: true,
|
||||
conversationId,
|
||||
conversationId: group.id,
|
||||
expireTimer: DAY / SECOND,
|
||||
expirationStartTimestamp: Date.now(),
|
||||
id: UUID.generate().toString(),
|
||||
|
@ -255,7 +277,7 @@ export async function sendStoryMessage(
|
|||
type: 'story',
|
||||
});
|
||||
|
||||
groupV2MessagesByConversationId.set(conversationId, messageAttributes);
|
||||
groupV2MessagesByConversationId.set(group.id, messageAttributes);
|
||||
})
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue