- {storiesToView &&
+ {conversationIdToView &&
renderStoryViewer({
- conversationId: storiesToView.conversationId,
- onClose: () => setStoriesToView(undefined),
+ conversationId: conversationIdToView,
+ onClose: () => setConversationIdToView(undefined),
onNextUserStories: () => {
const storyIndex = stories.findIndex(
- x => x.conversationId === storiesToView.conversationId
+ x => x.conversationId === conversationIdToView
);
if (storyIndex >= stories.length - 1) {
- setStoriesToView(undefined);
+ setConversationIdToView(undefined);
return;
}
const nextStory = stories[storyIndex + 1];
- setStoriesToView({
- conversationId: nextStory.conversationId,
- stories: nextStory.stories,
- });
+ setConversationIdToView(nextStory.conversationId);
},
onPrevUserStories: () => {
const storyIndex = stories.findIndex(
- x => x.conversationId === storiesToView.conversationId
+ x => x.conversationId === conversationIdToView
);
if (storyIndex === 0) {
- setStoriesToView(undefined);
+ setConversationIdToView(undefined);
return;
}
const prevStory = stories[storyIndex - 1];
- setStoriesToView({
- conversationId: prevStory.conversationId,
- stories: prevStory.stories,
- });
+ setConversationIdToView(prevStory.conversationId);
},
- stories: storiesToView.stories,
})}
-
+
{i18n('Stories__placeholder--text')}
diff --git a/ts/components/StoriesPane.tsx b/ts/components/StoriesPane.tsx
index e082959bd7bb..674cce8c7f3b 100644
--- a/ts/components/StoriesPane.tsx
+++ b/ts/components/StoriesPane.tsx
@@ -111,8 +111,9 @@ export const StoriesPane = ({
>
{renderedStories.map(story => (
{
onStoryClicked(story.conversationId);
}}
diff --git a/ts/components/StoryListItem.stories.tsx b/ts/components/StoryListItem.stories.tsx
index eff359e24720..91513960a179 100644
--- a/ts/components/StoryListItem.stories.tsx
+++ b/ts/components/StoryListItem.stories.tsx
@@ -63,7 +63,7 @@ story.add('My Story (many)', () => (
story.add("Someone's story", () => (
;
+ group?: Pick<
+ ConversationType,
+ | 'acceptedMessageRequest'
+ | 'avatarPath'
+ | 'color'
+ | 'id'
+ | 'name'
+ | 'profileName'
+ | 'sharedGroupNames'
+ | 'title'
+ >;
hasMultiple?: boolean;
isHidden?: boolean;
searchNames?: string; // This is just here to satisfy Fuse's types
@@ -24,6 +34,7 @@ export type ConversationStoryType = {
export type StoryViewType = {
attachment?: AttachmentType;
+ canReply?: boolean;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
isHidden?: boolean;
diff --git a/ts/components/StoryViewer.stories.tsx b/ts/components/StoryViewer.stories.tsx
index 64610153d30d..0983ae0481de 100644
--- a/ts/components/StoryViewer.stories.tsx
+++ b/ts/components/StoryViewer.stories.tsx
@@ -17,10 +17,14 @@ const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/StoryViewer', module);
function getDefaultProps(): PropsType {
+ const sender = getDefaultConversation();
+
return {
+ conversationId: sender.id,
getPreferredBadge: () => undefined,
group: undefined,
i18n,
+ loadStoryReplies: action('loadStoryReplies'),
markStoryRead: action('markStoryRead'),
onClose: action('onClose'),
onNextUserStories: action('onNextUserStories'),
@@ -33,18 +37,16 @@ function getDefaultProps(): PropsType {
preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'],
queueStoryDownload: action('queueStoryDownload'),
renderEmojiPicker: () => ,
- replies: Math.floor(Math.random() * 20),
stories: [
{
attachment: fakeAttachment({
url: '/fixtures/snow.jpg',
}),
messageId: '123',
- sender: getDefaultConversation(),
+ sender,
timestamp: Date.now(),
},
],
- views: Math.floor(Math.random() * 20),
};
}
diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx
index 9a1128d82a46..bbb2be2724a0 100644
--- a/ts/components/StoryViewer.tsx
+++ b/ts/components/StoryViewer.tsx
@@ -9,6 +9,7 @@ import type { ConversationType } from '../state/ducks/conversations';
import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
+import type { ReplyStateType } from '../types/Stories';
import type { StoryViewType } from './StoryListItem';
import { Avatar, AvatarSize } from './Avatar';
import { Intl } from './Intl';
@@ -22,9 +23,21 @@ import { isDownloaded, isDownloading } from '../types/Attachment';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
export type PropsType = {
+ conversationId: string;
getPreferredBadge: PreferredBadgeSelectorType;
- group?: ConversationType;
+ group?: Pick<
+ ConversationType,
+ | 'acceptedMessageRequest'
+ | 'avatarPath'
+ | 'color'
+ | 'id'
+ | 'name'
+ | 'profileName'
+ | 'sharedGroupNames'
+ | 'title'
+ >;
i18n: LocalizerType;
+ loadStoryReplies: (conversationId: string, messageId: string) => unknown;
markStoryRead: (mId: string) => unknown;
onClose: () => unknown;
onNextUserStories: () => unknown;
@@ -42,11 +55,10 @@ export type PropsType = {
preferredReactionEmoji: Array;
queueStoryDownload: (storyId: string) => unknown;
recentEmojis?: Array;
- replies?: number;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
+ replyState?: ReplyStateType;
skinTone?: number;
stories: Array;
- views?: number;
};
const CAPTION_BUFFER = 20;
@@ -54,9 +66,11 @@ const CAPTION_INITIAL_LENGTH = 200;
const CAPTION_MAX_LENGTH = 700;
export const StoryViewer = ({
+ conversationId,
getPreferredBadge,
group,
i18n,
+ loadStoryReplies,
markStoryRead,
onClose,
onNextUserStories,
@@ -70,17 +84,16 @@ export const StoryViewer = ({
queueStoryDownload,
recentEmojis,
renderEmojiPicker,
- replies,
+ replyState,
skinTone,
stories,
- views,
}: PropsType): JSX.Element => {
const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
const [storyDuration, setStoryDuration] = useState();
const visibleStory = stories[currentStoryIndex];
- const { attachment, messageId, timestamp } = visibleStory;
+ const { attachment, canReply, messageId, timestamp } = visibleStory;
const {
acceptedMessageRequest,
avatarPath,
@@ -240,6 +253,20 @@ export const StoryViewer = ({
};
}, [navigateStories]);
+ const isGroupStory = Boolean(group?.id);
+ useEffect(() => {
+ if (!isGroupStory) {
+ return;
+ }
+ loadStoryReplies(conversationId, messageId);
+ }, [conversationId, isGroupStory, loadStoryReplies, messageId]);
+
+ const replies =
+ replyState && replyState.messageId === messageId ? replyState.replies : [];
+
+ const viewCount = 0;
+ const replyCount = replies.length;
+
return (
@@ -366,49 +393,51 @@ export const StoryViewer = ({
{isMe ? (
<>
- {views &&
- (views === 1 ? (
+ {viewCount &&
+ (viewCount === 1 ? (
{views}]}
+ components={[{viewCount}]}
/>
) : (
{views}]}
+ components={[{viewCount}]}
/>
))}
- {views && replies && ' '}
- {replies &&
- (replies === 1 ? (
+ {viewCount && replyCount && ' '}
+ {replyCount &&
+ (replyCount === 1 ? (
{replies}]}
+ components={[{replyCount}]}
/>
) : (
{replies}]}
+ components={[{replyCount}]}
/>
))}
>
) : (
-
+ canReply && (
+
+ )
)}
- {hasReplyModal && (
+ {hasReplyModal && canReply && (
& {
- body?: string;
- contactNameColor?: ContactNameColorType;
- reactionEmoji?: string;
- timestamp: number;
-};
-
type ViewType = Pick<
ConversationType,
| 'acceptedMessageRequest'
@@ -223,7 +208,7 @@ export const StoryViewsNRepliesModal = ({
{replies.map(reply =>
reply.reactionEmoji ? (
-
+
) : (
-
+
-
+
-
+
{tabsElement || (
<>
diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx
index 8c626a8979cd..5581d46afb64 100644
--- a/ts/components/conversation/Quote.tsx
+++ b/ts/components/conversation/Quote.tsx
@@ -47,7 +47,7 @@ type State = {
export type QuotedAttachmentType = Pick<
AttachmentType,
- 'contentType' | 'fileName' | 'isVoiceMessage' | 'thumbnail'
+ 'contentType' | 'fileName' | 'isVoiceMessage' | 'thumbnail' | 'textAttachment'
>;
function validateQuote(quote: Props): boolean {
@@ -221,10 +221,11 @@ export class Quote extends React.Component {
return null;
}
- const { fileName, contentType } = attachment;
+ const { fileName, contentType, textAttachment } = attachment;
const isGenericFile =
!GoogleChrome.isVideoTypeSupported(contentType) &&
!GoogleChrome.isImageTypeSupported(contentType) &&
+ !textAttachment &&
!MIME.isAudio(contentType);
if (!isGenericFile) {
@@ -257,13 +258,18 @@ export class Quote extends React.Component {
return null;
}
- const { contentType, thumbnail } = attachment;
+ const { contentType, textAttachment, thumbnail } = attachment;
const url = getUrl(thumbnail);
if (isViewOnce) {
return this.renderIcon('view-once');
}
+ // TODO DESKTOP-3433
+ if (textAttachment) {
+ return this.renderIcon('image');
+ }
+
if (GoogleChrome.isVideoTypeSupported(contentType)) {
return url && !imageBroken
? this.renderImage(url, 'play')
diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts
index b4f6b6eadb3f..3a3282faad07 100644
--- a/ts/models/conversations.ts
+++ b/ts/models/conversations.ts
@@ -3958,7 +3958,7 @@ export class ConversationModel extends window.Backbone
storyId?: string;
timestamp?: number;
} = {}
- ): Promise {
+ ): Promise {
if (this.isGroupV1AndDisabled()) {
return;
}
@@ -4143,6 +4143,8 @@ export class ConversationModel extends window.Backbone
}
window.Signal.Data.updateConversation(this.attributes);
+
+ return attributes;
}
// Is this someone who is a contact, or are we sharing our profile with them?
diff --git a/ts/services/storyLoader.ts b/ts/services/storyLoader.ts
index fdc5e3e7148a..a6c473568602 100644
--- a/ts/services/storyLoader.ts
+++ b/ts/services/storyLoader.ts
@@ -43,10 +43,13 @@ export function getStoryDataFromMessageAttributes(
selectedReaction,
...pick(message, [
'conversationId',
+ 'deletedForEveryone',
'readStatus',
+ 'sendStateByConversationId',
'source',
'sourceUuid',
'timestamp',
+ 'type',
]),
};
}
diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts
index c320d3b03e67..3f6876de6cbe 100644
--- a/ts/sql/Client.ts
+++ b/ts/sql/Client.ts
@@ -1238,7 +1238,7 @@ async function getOlderMessagesByConversation(
messageId?: string;
receivedAt?: number;
sentAt?: number;
- storyId?: UUIDStringType;
+ storyId?: string;
}
) {
const messages = await channels.getOlderMessagesByConversation(
diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts
index e4660e016d9d..c61d454f1a1e 100644
--- a/ts/sql/Interface.ts
+++ b/ts/sql/Interface.ts
@@ -617,7 +617,7 @@ export type ServerInterface = DataInterface & {
messageId?: string;
receivedAt?: number;
sentAt?: number;
- storyId?: UUIDStringType;
+ storyId?: string;
}
) => Promise>;
getNewerMessagesByConversation: (
@@ -687,7 +687,7 @@ export type ClientInterface = DataInterface & {
messageId?: string;
receivedAt?: number;
sentAt?: number;
- storyId?: UUIDStringType;
+ storyId?: string;
}
) => Promise>;
getNewerMessagesByConversation: (
diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts
index 4fc1f913d8b2..bf475fd05db2 100644
--- a/ts/sql/Server.ts
+++ b/ts/sql/Server.ts
@@ -2314,7 +2314,7 @@ async function getOlderMessagesByConversation(
messageId?: string;
receivedAt?: number;
sentAt?: number;
- storyId?: UUIDStringType;
+ storyId?: string;
}
): Promise> {
return getOlderMessagesByConversationSync(conversationId, options);
@@ -2332,7 +2332,7 @@ function getOlderMessagesByConversationSync(
messageId?: string;
receivedAt?: number;
sentAt?: number;
- storyId?: UUIDStringType;
+ storyId?: string;
} = {}
): Array {
const db = getInstance();
diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts
index 8bd7c5bc7fe4..1c2f44c8a0e7 100644
--- a/ts/state/ducks/stories.ts
+++ b/ts/state/ducks/stories.ts
@@ -6,7 +6,10 @@ import { pick } from 'lodash';
import type { AttachmentType } from '../../types/Attachment';
import type { BodyRangeType } from '../../types/Util';
import type { MessageAttributesType } from '../../model-types.d';
-import type { MessageDeletedActionType } from './conversations';
+import type {
+ MessageChangedActionType,
+ MessageDeletedActionType,
+} from './conversations';
import type { NoopActionType } from './noop';
import type { StateType as RootStateType } from '../reducer';
import type { StoryViewType } from '../../components/StoryListItem';
@@ -33,23 +36,44 @@ export type StoryDataType = {
selectedReaction?: string;
} & Pick<
MessageAttributesType,
- 'conversationId' | 'readStatus' | 'source' | 'sourceUuid' | 'timestamp'
+ | 'conversationId'
+ | 'deletedForEveryone'
+ | 'readStatus'
+ | 'sendStateByConversationId'
+ | 'source'
+ | 'sourceUuid'
+ | 'timestamp'
+ | 'type'
>;
// State
export type StoriesStateType = {
readonly isShowingStoriesView: boolean;
+ readonly replyState?: {
+ messageId: string;
+ replies: Array;
+ };
readonly stories: Array;
};
// Actions
+const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
const REACT_TO_STORY = 'stories/REACT_TO_STORY';
+const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
const STORY_CHANGED = 'stories/STORY_CHANGED';
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
+type LoadStoryRepliesActionType = {
+ type: typeof LOAD_STORY_REPLIES;
+ payload: {
+ messageId: string;
+ replies: Array;
+ };
+};
+
type MarkStoryReadActionType = {
type: typeof MARK_STORY_READ;
payload: string;
@@ -63,6 +87,11 @@ type ReactToStoryActionType = {
};
};
+type ReplyToStoryActionType = {
+ type: typeof REPLY_TO_STORY;
+ payload: MessageAttributesType;
+};
+
type StoryChangedActionType = {
type: typeof STORY_CHANGED;
payload: StoryDataType;
@@ -73,15 +102,19 @@ type ToggleViewActionType = {
};
export type StoriesActionType =
+ | LoadStoryRepliesActionType
| MarkStoryReadActionType
+ | MessageChangedActionType
| MessageDeletedActionType
| ReactToStoryActionType
+ | ReplyToStoryActionType
| StoryChangedActionType
| ToggleViewActionType;
// Action Creators
export const actions = {
+ loadStoryReplies,
markStoryRead,
queueStoryDownload,
reactToStory,
@@ -92,6 +125,26 @@ export const actions = {
export const useStoriesActions = (): typeof actions => useBoundActions(actions);
+function loadStoryReplies(
+ conversationId: string,
+ messageId: string
+): ThunkAction {
+ return async dispatch => {
+ const replies = await dataInterface.getOlderMessagesByConversation(
+ conversationId,
+ { limit: 9000, storyId: messageId }
+ );
+
+ dispatch({
+ type: LOAD_STORY_REPLIES,
+ payload: {
+ messageId,
+ replies,
+ },
+ });
+ };
+}
+
function markStoryRead(
messageId: string
): ThunkAction {
@@ -225,11 +278,16 @@ function replyToStory(
mentions: Array,
timestamp: number,
story: StoryViewType
-): NoopActionType {
- const conversation = window.ConversationController.get(conversationId);
+): ThunkAction {
+ return async dispatch => {
+ const conversation = window.ConversationController.get(conversationId);
- if (conversation) {
- conversation.enqueueMessageForSend(
+ if (!conversation) {
+ log.error('replyToStory: conversation does not exist', conversationId);
+ return;
+ }
+
+ const messageAttributes = await conversation.enqueueMessageForSend(
{
body: messageBody,
attachments: [],
@@ -240,11 +298,13 @@ function replyToStory(
timestamp,
}
);
- }
- return {
- type: 'NOOP',
- payload: null,
+ if (messageAttributes) {
+ dispatch({
+ type: REPLY_TO_STORY,
+ payload: messageAttributes,
+ });
+ }
};
}
@@ -285,11 +345,17 @@ export function reducer(
}
if (action.type === 'MESSAGE_DELETED') {
+ const nextStories = state.stories.filter(
+ story => story.messageId !== action.payload.id
+ );
+
+ if (nextStories.length === state.stories.length) {
+ return state;
+ }
+
return {
...state,
- stories: state.stories.filter(
- story => story.messageId !== action.payload.id
- ),
+ stories: nextStories,
};
}
@@ -297,12 +363,15 @@ export function reducer(
const newStory = pick(action.payload, [
'attachment',
'conversationId',
+ 'deletedForEveryone',
'messageId',
'readStatus',
'selectedReaction',
+ 'sendStateByConversationId',
'source',
'sourceUuid',
'timestamp',
+ 'type',
]);
// Stories don't really need to change except for when we don't have the
@@ -326,6 +395,10 @@ export function reducer(
existingStory => existingStory.messageId === newStory.messageId
);
+ if (storyIndex < 0) {
+ return state;
+ }
+
return {
...state,
stories: replaceIndex(state.stories, storyIndex, newStory),
@@ -374,5 +447,63 @@ export function reducer(
};
}
+ if (action.type === LOAD_STORY_REPLIES) {
+ return {
+ ...state,
+ replyState: action.payload,
+ };
+ }
+
+ // For live updating of the story replies
+ if (
+ action.type === 'MESSAGE_CHANGED' &&
+ state.replyState &&
+ state.replyState.messageId === action.payload.data.storyId
+ ) {
+ const { replyState } = state;
+ const messageIndex = replyState.replies.findIndex(
+ reply => reply.id === action.payload.id
+ );
+
+ // New message
+ if (messageIndex < 0) {
+ return {
+ ...state,
+ replyState: {
+ messageId: replyState.messageId,
+ replies: [...replyState.replies, action.payload.data],
+ },
+ };
+ }
+
+ // Changed message, also handles DOE
+ return {
+ ...state,
+ replyState: {
+ messageId: replyState.messageId,
+ replies: replaceIndex(
+ replyState.replies,
+ messageIndex,
+ action.payload.data
+ ),
+ },
+ };
+ }
+
+ if (action.type === REPLY_TO_STORY) {
+ const { replyState } = state;
+ if (!replyState) {
+ return state;
+ }
+
+ return {
+ ...state,
+ replyState: {
+ messageId: replyState.messageId,
+ replies: [...replyState.replies, action.payload],
+ },
+ };
+ }
+
return state;
}
diff --git a/ts/state/selectors/stories.ts b/ts/state/selectors/stories.ts
index c167aad2d7d4..974dbdf366fc 100644
--- a/ts/state/selectors/stories.ts
+++ b/ts/state/selectors/stories.ts
@@ -4,14 +4,22 @@
import { createSelector } from 'reselect';
import { pick } from 'lodash';
+import type { GetConversationByIdType } from './conversations';
import type {
ConversationStoryType,
StoryViewType,
} from '../../components/StoryListItem';
+import type { ReplyStateType } from '../../types/Stories';
import type { StateType } from '../reducer';
-import type { StoriesStateType } from '../ducks/stories';
+import type { StoryDataType, StoriesStateType } from '../ducks/stories';
import { ReadStatus } from '../../messages/MessageReadStatus';
-import { getConversationSelector } from './conversations';
+import { canReply } from './message';
+import {
+ getContactNameColorSelector,
+ getConversationSelector,
+ getMe,
+} from './conversations';
+import { getUserConversationId } from './user';
export const getStoriesState = (state: StateType): StoriesStateType =>
state.stories;
@@ -47,12 +55,148 @@ function sortByRecencyAndUnread(
return storyA.timestamp > storyB.timestamp ? -1 : 1;
}
+function getConversationStory(
+ conversationSelector: GetConversationByIdType,
+ story: StoryDataType,
+ ourConversationId?: string
+): ConversationStoryType {
+ const sender = pick(conversationSelector(story.sourceUuid || story.source), [
+ 'acceptedMessageRequest',
+ 'avatarPath',
+ 'color',
+ 'firstName',
+ 'hideStory',
+ 'id',
+ 'isMe',
+ 'name',
+ 'profileName',
+ 'sharedGroupNames',
+ 'title',
+ ]);
+
+ const conversation = pick(conversationSelector(story.conversationId), [
+ 'acceptedMessageRequest',
+ 'avatarPath',
+ 'color',
+ 'id',
+ 'name',
+ 'profileName',
+ 'sharedGroupNames',
+ 'title',
+ ]);
+
+ const { attachment, timestamp } = pick(story, ['attachment', 'timestamp']);
+
+ const storyView: StoryViewType = {
+ attachment,
+ canReply: canReply(story, ourConversationId, conversationSelector),
+ isUnread: story.readStatus === ReadStatus.Unread,
+ messageId: story.messageId,
+ selectedReaction: story.selectedReaction,
+ sender,
+ timestamp,
+ };
+
+ return {
+ conversationId: conversation.id,
+ group: conversation.id !== sender.id ? conversation : undefined,
+ isHidden: Boolean(sender.hideStory),
+ stories: [storyView],
+ };
+}
+
+export type GetStoriesByConversationIdType = (
+ conversationId: string
+) => ConversationStoryType;
+export const getStoriesSelector = createSelector(
+ getConversationSelector,
+ getUserConversationId,
+ getStoriesState,
+ (
+ conversationSelector,
+ ourConversationId,
+ { stories }: Readonly
+ ): GetStoriesByConversationIdType => {
+ return conversationId => {
+ const conversationStoryAcc: ConversationStoryType = {
+ conversationId,
+ stories: [],
+ };
+
+ return stories.reduce((acc, story) => {
+ if (story.conversationId !== conversationId) {
+ return acc;
+ }
+
+ const conversationStory = getConversationStory(
+ conversationSelector,
+ story,
+ ourConversationId
+ );
+
+ return {
+ ...acc,
+ ...conversationStory,
+ stories: [...acc.stories, ...conversationStory.stories],
+ };
+ }, conversationStoryAcc);
+ };
+ }
+);
+
+export const getStoryReplies = createSelector(
+ getConversationSelector,
+ getContactNameColorSelector,
+ getMe,
+ getStoriesState,
+ (
+ conversationSelector,
+ contactNameColorSelector,
+ me,
+ { replyState }: Readonly
+ ): ReplyStateType | undefined => {
+ if (!replyState) {
+ return;
+ }
+
+ return {
+ messageId: replyState.messageId,
+ replies: replyState.replies.map(reply => {
+ const conversation =
+ reply.type === 'outgoing'
+ ? me
+ : conversationSelector(reply.sourceUuid || reply.source);
+
+ return {
+ ...pick(conversation, [
+ 'acceptedMessageRequest',
+ 'avatarPath',
+ 'color',
+ 'isMe',
+ 'name',
+ 'profileName',
+ 'sharedGroupNames',
+ 'title',
+ ]),
+ ...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']),
+ contactNameColor: contactNameColorSelector(
+ reply.conversationId,
+ conversation.id
+ ),
+ };
+ }),
+ };
+ }
+);
+
export const getStories = createSelector(
getConversationSelector,
+ getUserConversationId,
getStoriesState,
shouldShowStoriesView,
(
conversationSelector,
+ ourConversationId,
{ stories }: Readonly,
isShowingStoriesView
): {
@@ -70,58 +214,30 @@ export const getStories = createSelector(
const hiddenStoriesById = new Map();
stories.forEach(story => {
- const sender = pick(
- conversationSelector(story.sourceUuid || story.source),
- [
- 'acceptedMessageRequest',
- 'avatarPath',
- 'color',
- 'firstName',
- 'hideStory',
- 'id',
- 'isMe',
- 'name',
- 'profileName',
- 'sharedGroupNames',
- 'title',
- ]
+ const conversationStory = getConversationStory(
+ conversationSelector,
+ story,
+ ourConversationId
);
- const conversation = pick(conversationSelector(story.conversationId), [
- 'id',
- 'title',
- ]);
-
- const { attachment, timestamp } = pick(story, [
- 'attachment',
- 'timestamp',
- ]);
-
let storiesMap: Map;
- if (sender.hideStory) {
+ if (conversationStory.isHidden) {
storiesMap = hiddenStoriesById;
} else {
storiesMap = storiesById;
}
- const storyView: StoryViewType = {
- attachment,
- isUnread: story.readStatus === ReadStatus.Unread,
- messageId: story.messageId,
- selectedReaction: story.selectedReaction,
- sender,
- timestamp,
- };
+ const existingConversationStory = storiesMap.get(
+ conversationStory.conversationId
+ ) || { stories: [] };
- const conversationStory = storiesMap.get(conversation.id) || {
- conversationId: conversation.id,
- group: conversation.id !== sender.id ? conversation : undefined,
- isHidden: Boolean(sender.hideStory),
- stories: [],
- };
- storiesMap.set(conversation.id, {
+ storiesMap.set(conversationStory.conversationId, {
+ ...existingConversationStory,
...conversationStory,
- stories: [...conversationStory.stories, storyView],
+ stories: [
+ ...existingConversationStory.stories,
+ ...conversationStory.stories,
+ ],
});
});
diff --git a/ts/state/smart/Stories.tsx b/ts/state/smart/Stories.tsx
index 3d3087f58d03..f451929be2cd 100644
--- a/ts/state/smart/Stories.tsx
+++ b/ts/state/smart/Stories.tsx
@@ -20,7 +20,6 @@ function renderStoryViewer({
onClose,
onNextUserStories,
onPrevUserStories,
- stories,
}: SmartStoryViewerPropsType): JSX.Element {
return (
);
}
diff --git a/ts/state/smart/StoryViewer.tsx b/ts/state/smart/StoryViewer.tsx
index 5ea125272fa5..06afed896a64 100644
--- a/ts/state/smart/StoryViewer.tsx
+++ b/ts/state/smart/StoryViewer.tsx
@@ -4,15 +4,16 @@
import React from 'react';
import { useSelector } from 'react-redux';
+import type { GetStoriesByConversationIdType } from '../selectors/stories';
import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
-import type { StoryViewType } from '../../components/StoryListItem';
import { StoryViewer } from '../../components/StoryViewer';
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
import {
getEmojiSkinTone,
getPreferredReactionEmoji,
} from '../selectors/items';
+import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
import { getIntl } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { renderEmojiPicker } from './renderEmojiPicker';
@@ -27,7 +28,6 @@ export type PropsType = {
onClose: () => unknown;
onNextUserStories: () => unknown;
onPrevUserStories: () => unknown;
- stories: Array;
};
export function SmartStoryViewer({
@@ -35,7 +35,6 @@ export function SmartStoryViewer({
onClose,
onNextUserStories,
onPrevUserStories,
- stories,
}: PropsType): JSX.Element | null {
const storiesActions = useStoriesActions();
const { onSetSkinTone } = useItemsActions();
@@ -47,12 +46,22 @@ export function SmartStoryViewer({
getPreferredReactionEmoji
);
+ const getStoriesByConversationId = useSelector<
+ StateType,
+ GetStoriesByConversationIdType
+ >(getStoriesSelector);
+
+ const { group, stories } = getStoriesByConversationId(conversationId);
+
const recentEmojis = useRecentEmojis();
const skinTone = useSelector(getEmojiSkinTone);
+ const replyState = useSelector(getStoryReplies);
return (
& {
+ body?: string;
+ contactNameColor?: ContactNameColorType;
+ deletedForEveryone?: boolean;
+ id: string;
+ reactionEmoji?: string;
+ timestamp: number;
+};
+
+export type ReplyStateType = {
+ messageId: string;
+ replies: Array;
+};