signal-desktop/ts/state/ducks/stories.ts

1637 lines
44 KiB
TypeScript
Raw Normal View History

// Copyright 2021-2022 Signal Messenger, LLC
2022-03-04 21:14:52 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2022-07-06 19:06:20 +00:00
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { isEqual, pick } from 'lodash';
import * as Errors from '../../types/errors';
2022-08-04 19:23:24 +00:00
import type { AttachmentType } from '../../types/Attachment';
2022-03-04 21:14:52 +00:00
import type { BodyRangeType } from '../../types/Util';
import type { ConversationModel } from '../../models/conversations';
2022-03-04 21:14:52 +00:00
import type { MessageAttributesType } from '../../model-types.d';
2022-04-15 00:08:46 +00:00
import type {
MessageChangedActionType,
MessageDeletedActionType,
2022-08-02 19:31:55 +00:00
MessagesAddedActionType,
2022-04-15 00:08:46 +00:00
} from './conversations';
2022-03-04 21:14:52 +00:00
import type { NoopActionType } from './noop';
import type { StateType as RootStateType } from '../reducer';
2022-10-11 17:59:02 +00:00
import type { StoryViewTargetType, StoryViewType } from '../../types/Stories';
2022-03-04 21:14:52 +00:00
import type { SyncType } from '../../jobs/helpers/syncHelpers';
2022-08-02 19:31:55 +00:00
import type { UUIDStringType } from '../../types/UUID';
2022-03-04 21:14:52 +00:00
import * as log from '../../logging/log';
2022-03-29 01:10:08 +00:00
import dataInterface from '../../sql/Client';
2022-03-04 21:14:52 +00:00
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
2022-07-06 19:06:20 +00:00
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
2022-03-04 21:14:52 +00:00
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
import { assertDev } from '../../util/assert';
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
2022-11-04 13:22:07 +00:00
import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone';
2022-03-04 21:14:52 +00:00
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById';
import { markViewed } from '../../services/MessageUpdater';
2022-03-29 01:10:08 +00:00
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { replaceIndex } from '../../util/replaceIndex';
2022-03-04 21:14:52 +00:00
import { showToast } from '../../util/showToast';
import { hasFailed, isDownloaded, isDownloading } from '../../types/Attachment';
2022-09-22 00:55:23 +00:00
import {
getConversationSelector,
getHideStoryConversationIds,
} from '../selectors/conversations';
import {
getStories,
getStoryDownloadableAttachment,
} from '../selectors/stories';
2022-08-02 19:31:55 +00:00
import { getStoryDataFromMessageAttributes } from '../../services/storyLoader';
2022-07-06 19:06:20 +00:00
import { isGroup } from '../../util/whatTypeOfConversation';
2022-08-02 19:31:55 +00:00
import { isNotNil } from '../../util/isNotNil';
import { isStory } from '../../messages/helpers';
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
2022-03-04 21:14:52 +00:00
import { useBoundActions } from '../../hooks/useBoundActions';
import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers';
2022-03-04 21:14:52 +00:00
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
export type StoryDataType = {
attachment?: AttachmentType;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
2022-03-04 21:14:52 +00:00
messageId: string;
startedDownload?: boolean;
2022-03-04 21:14:52 +00:00
} & Pick<
MessageAttributesType,
2022-07-01 00:52:03 +00:00
| 'canReplyToStory'
2022-04-15 00:08:46 +00:00
| 'conversationId'
| 'deletedForEveryone'
2022-04-28 22:06:28 +00:00
| 'reactions'
2022-09-21 23:54:48 +00:00
| 'readAt'
2022-04-15 00:08:46 +00:00
| 'readStatus'
| 'sendStateByConversationId'
| 'source'
| 'sourceUuid'
| 'sourceDevice'
2022-07-01 00:52:03 +00:00
| 'storyDistributionListId'
2022-04-15 00:08:46 +00:00
| 'timestamp'
| 'type'
> & {
// don't want the fields to be optional as in MessageAttributesType
expireTimer: number | undefined;
expirationStartTimestamp: number | undefined;
};
2022-03-04 21:14:52 +00:00
2022-07-06 19:06:20 +00:00
export type SelectedStoryDataType = {
currentIndex: number;
messageId: string;
2022-07-06 19:06:20 +00:00
numStories: number;
2022-08-22 17:44:23 +00:00
storyViewMode: StoryViewModeType;
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted: Array<string>;
viewTarget?: StoryViewTargetType;
2022-07-06 19:06:20 +00:00
};
export type AddStoryData =
| {
type: 'Media';
file: File;
sending?: boolean;
}
| {
type: 'Text';
sending?: boolean;
}
| undefined;
2022-03-04 21:14:52 +00:00
// State
export type StoriesStateType = {
readonly lastOpenedAtTimestamp: number | undefined;
readonly openedAtTimestamp: number | undefined;
2022-04-15 00:08:46 +00:00
readonly replyState?: {
messageId: string;
replies: Array<MessageAttributesType>;
};
2022-07-06 19:06:20 +00:00
readonly selectedStoryData?: SelectedStoryDataType;
readonly addStoryData: AddStoryData;
readonly sendStoryModalData?: {
untrustedUuids: Array<string>;
verifiedUuids: Array<string>;
};
2022-03-04 21:14:52 +00:00
readonly stories: Array<StoryDataType>;
};
// Actions
2022-07-01 00:52:03 +00:00
const DOE_STORY = 'stories/DOE';
const LIST_MEMBERS_VERIFIED = 'stories/LIST_MEMBERS_VERIFIED';
2022-04-15 00:08:46 +00:00
const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
2022-03-29 01:10:08 +00:00
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
const QUEUE_STORY_DOWNLOAD = 'stories/QUEUE_STORY_DOWNLOAD';
const SEND_STORY_MODAL_OPEN_STATE_CHANGED =
'stories/SEND_STORY_MODAL_OPEN_STATE_CHANGED';
2022-03-29 01:10:08 +00:00
const STORY_CHANGED = 'stories/STORY_CHANGED';
2022-03-04 21:14:52 +00:00
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
2022-07-06 19:06:20 +00:00
const VIEW_STORY = 'stories/VIEW_STORY';
2022-11-04 13:22:07 +00:00
const STORY_REPLY_DELETED = 'stories/STORY_REPLY_DELETED';
const REMOVE_ALL_STORIES = 'stories/REMOVE_ALL_STORIES';
const SET_ADD_STORY_DATA = 'stories/SET_ADD_STORY_DATA';
const SET_STORY_SENDING = 'stories/SET_STORY_SENDING';
2022-03-04 21:14:52 +00:00
2022-07-01 00:52:03 +00:00
type DOEStoryActionType = {
type: typeof DOE_STORY;
payload: string;
};
type ListMembersVerified = {
type: typeof LIST_MEMBERS_VERIFIED;
payload: {
untrustedUuids: Array<string>;
verifiedUuids: Array<string>;
};
};
2022-04-15 00:08:46 +00:00
type LoadStoryRepliesActionType = {
type: typeof LOAD_STORY_REPLIES;
payload: {
messageId: string;
replies: Array<MessageAttributesType>;
};
};
2022-03-29 01:10:08 +00:00
type MarkStoryReadActionType = {
type: typeof MARK_STORY_READ;
2022-09-21 23:54:48 +00:00
payload: {
messageId: string;
readAt: number;
};
2022-03-04 21:14:52 +00:00
};
type QueueStoryDownloadActionType = {
type: typeof QUEUE_STORY_DOWNLOAD;
payload: string;
};
type SendStoryModalOpenStateChanged = {
type: typeof SEND_STORY_MODAL_OPEN_STATE_CHANGED;
payload: number | undefined;
};
2022-03-29 01:10:08 +00:00
type StoryChangedActionType = {
type: typeof STORY_CHANGED;
payload: StoryDataType;
};
2022-03-04 21:14:52 +00:00
type ToggleViewActionType = {
type: typeof TOGGLE_VIEW;
};
2022-07-06 19:06:20 +00:00
type ViewStoryActionType = {
type: typeof VIEW_STORY;
2022-08-22 17:44:23 +00:00
payload: SelectedStoryDataType | undefined;
2022-07-06 19:06:20 +00:00
};
2022-11-04 13:22:07 +00:00
type StoryReplyDeletedActionType = {
type: typeof STORY_REPLY_DELETED;
payload: string;
};
type RemoveAllStoriesActionType = {
type: typeof REMOVE_ALL_STORIES;
};
type SetAddStoryDataType = {
type: typeof SET_ADD_STORY_DATA;
payload: AddStoryData;
};
type SetStorySendingType = {
type: typeof SET_STORY_SENDING;
payload: boolean;
};
2022-03-04 21:14:52 +00:00
export type StoriesActionType =
2022-07-01 00:52:03 +00:00
| DOEStoryActionType
| ListMembersVerified
2022-04-15 00:08:46 +00:00
| LoadStoryRepliesActionType
2022-03-29 01:10:08 +00:00
| MarkStoryReadActionType
2022-04-15 00:08:46 +00:00
| MessageChangedActionType
2022-03-04 21:14:52 +00:00
| MessageDeletedActionType
2022-08-02 19:31:55 +00:00
| MessagesAddedActionType
| QueueStoryDownloadActionType
| SendStoryModalOpenStateChanged
2022-03-29 01:10:08 +00:00
| StoryChangedActionType
2022-07-06 19:06:20 +00:00
| ToggleViewActionType
| ViewStoryActionType
2022-11-04 13:22:07 +00:00
| StoryReplyDeletedActionType
| RemoveAllStoriesActionType
| SetAddStoryDataType
| SetStorySendingType;
2022-03-04 21:14:52 +00:00
// Action Creators
2022-11-04 13:22:07 +00:00
function deleteGroupStoryReply(
messageId: string
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
return async dispatch => {
await window.Signal.Data.removeMessage(messageId);
dispatch({
type: STORY_REPLY_DELETED,
payload: messageId,
});
};
}
function deleteGroupStoryReplyForEveryone(
replyMessageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
await doDeleteGroupStoryReplyForEveryone(replyMessageId);
// the call above re-uses the sync-message processing code to update the UI
// we don't need to do anything here
dispatch({
type: 'NOOP',
payload: null,
});
};
}
2022-07-01 00:52:03 +00:00
function deleteStoryForEveryone(
story: StoryViewType
): ThunkAction<void, RootStateType, unknown, DOEStoryActionType> {
2022-07-13 23:09:18 +00:00
return async (dispatch, getState) => {
2022-07-01 00:52:03 +00:00
if (!story.sendState) {
return;
}
const { stories } = getState().stories;
const storyData = stories.find(item => item.messageId === story.messageId);
if (!storyData) {
log.warn('deleteStoryForEveryone: Could not find story in redux data');
return;
2022-07-13 23:09:18 +00:00
}
await doDeleteStoryForEveryone(stories, storyData);
2022-07-13 23:09:18 +00:00
2022-07-01 00:52:03 +00:00
dispatch({
type: DOE_STORY,
payload: story.messageId,
});
};
}
2022-04-15 00:08:46 +00:00
function loadStoryReplies(
conversationId: string,
messageId: string
): ThunkAction<void, RootStateType, unknown, LoadStoryRepliesActionType> {
return async (dispatch, getState) => {
const conversation = getConversationSelector(getState())(conversationId);
2022-04-15 00:08:46 +00:00
const replies = await dataInterface.getOlderMessagesByConversation(
conversationId,
{
limit: 9000,
storyId: messageId,
includeStoryReplies: !isGroup(conversation),
}
2022-04-15 00:08:46 +00:00
);
dispatch({
type: LOAD_STORY_REPLIES,
payload: {
messageId,
replies,
},
});
};
}
2022-03-04 21:14:52 +00:00
function markStoryRead(
messageId: string
2022-03-29 01:10:08 +00:00
): ThunkAction<void, RootStateType, unknown, MarkStoryReadActionType> {
2022-03-04 21:14:52 +00:00
return async (dispatch, getState) => {
const { stories } = getState().stories;
const matchingStory = stories.find(story => story.messageId === messageId);
if (!matchingStory) {
log.warn(`markStoryRead: no matching story found: ${messageId}`);
return;
}
2022-08-04 00:38:41 +00:00
if (
!isDownloaded(matchingStory.attachment) &&
!hasFailed(matchingStory.attachment)
) {
log.warn(
`markStoryRead: not downloaded: ${messageId} ${
matchingStory.attachment?.error
? `error: ${matchingStory.attachment?.error}`
: ''
}`
);
2022-04-08 15:40:15 +00:00
return;
}
2022-03-04 21:14:52 +00:00
if (matchingStory.readStatus !== ReadStatus.Unread) {
log.warn(
`markStoryRead: not unread, ${messageId} read status: ${matchingStory.readStatus}`
);
2022-03-04 21:14:52 +00:00
return;
}
const message = await getMessageById(messageId);
if (!message) {
log.warn(`markStoryRead: no message found ${messageId}`);
2022-03-04 21:14:52 +00:00
return;
}
2022-03-29 01:10:08 +00:00
const storyReadDate = Date.now();
message.set(markViewed(message.attributes, storyReadDate));
2022-03-04 21:14:52 +00:00
const viewedReceipt = {
messageId,
senderE164: message.attributes.source,
senderUuid: message.attributes.sourceUuid,
timestamp: message.attributes.sent_at,
2022-08-15 21:53:33 +00:00
isDirectConversation: false,
2022-03-04 21:14:52 +00:00
};
const viewSyncs: Array<SyncType> = [viewedReceipt];
if (!window.ConversationController.areWePrimaryDevice()) {
viewSyncJobQueue.add({ viewSyncs });
}
2022-10-25 22:18:42 +00:00
if (window.Events.getStoryViewReceiptsEnabled()) {
viewedReceiptsJobQueue.add({ viewedReceipt });
}
2022-03-04 21:14:52 +00:00
2022-03-29 01:10:08 +00:00
await dataInterface.addNewStoryRead({
authorId: message.attributes.sourceUuid,
conversationId: message.attributes.conversationId,
2022-07-08 20:46:25 +00:00
storyId: messageId,
2022-03-29 01:10:08 +00:00
storyReadDate,
});
dispatch({
type: MARK_STORY_READ,
2022-09-21 23:54:48 +00:00
payload: {
messageId,
readAt: storyReadDate,
},
2022-03-29 01:10:08 +00:00
});
};
}
function queueStoryDownload(
storyId: string
): ThunkAction<
void,
RootStateType,
unknown,
NoopActionType | QueueStoryDownloadActionType
> {
2022-07-01 00:52:03 +00:00
return async (dispatch, getState) => {
const { stories } = getState().stories;
const story = stories.find(item => item.messageId === storyId);
2022-03-29 01:10:08 +00:00
if (!story) {
return;
}
const attachment = getStoryDownloadableAttachment(story);
2022-03-29 01:10:08 +00:00
if (!attachment) {
log.warn('queueStoryDownload: No attachment found for story', {
storyId,
});
return;
}
2022-08-04 00:38:41 +00:00
if (hasFailed(attachment)) {
return;
}
2022-03-29 01:10:08 +00:00
if (isDownloaded(attachment)) {
if (!attachment.path) {
return;
}
2022-03-29 01:10:08 +00:00
return;
}
// isDownloading checks for the downloadJobId which is set by
// queueAttachmentDownloads but we optimistically set story.startedDownload
// in redux to prevent race conditions from queuing up multiple attachment
// downloads before the attachment save takes place.
if (isDownloading(attachment) || story.startedDownload) {
2022-03-29 01:10:08 +00:00
return;
}
2022-07-01 00:52:03 +00:00
const message = await getMessageById(storyId);
if (message) {
// We want to ensure that we re-hydrate the story reply context with the
// completed attachment download.
message.set({ storyReplyContext: undefined });
2022-03-29 01:10:08 +00:00
dispatch({
type: QUEUE_STORY_DOWNLOAD,
payload: storyId,
});
const updatedFields = await queueAttachmentDownloads(message.attributes);
if (updatedFields) {
message.set(updatedFields);
}
return;
2022-07-01 00:52:03 +00:00
}
2022-03-29 01:10:08 +00:00
2022-03-04 21:14:52 +00:00
dispatch({
type: 'NOOP',
payload: null,
});
};
}
function reactToStory(
nextReaction: string,
2022-04-28 22:06:28 +00:00
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
2022-03-04 21:14:52 +00:00
return async dispatch => {
try {
await enqueueReactionForSend({
messageId,
emoji: nextReaction,
2022-04-28 22:06:28 +00:00
remove: false,
2022-03-04 21:14:52 +00:00
});
} catch (error) {
log.error('Error enqueuing reaction', error, messageId, nextReaction);
showToast(ToastReactionFailed);
}
2022-04-28 22:06:28 +00:00
dispatch({
type: 'NOOP',
payload: null,
});
2022-03-04 21:14:52 +00:00
};
}
function replyToStory(
conversationId: string,
messageBody: string,
2022-03-04 21:14:52 +00:00
mentions: Array<BodyRangeType>,
timestamp: number,
story: StoryViewType
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
2022-04-15 00:08:46 +00:00
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
log.error('replyToStory: conversation does not exist', conversationId);
return;
}
2022-03-04 21:14:52 +00:00
await conversation.enqueueMessageForSend(
{
body: messageBody,
attachments: [],
mentions,
},
2022-03-04 21:14:52 +00:00
{
storyId: story.messageId,
timestamp,
}
);
dispatch({
type: 'NOOP',
payload: null,
});
2022-03-04 21:14:52 +00:00
};
}
2022-08-02 19:31:55 +00:00
function sendStoryMessage(
listIds: Array<UUIDStringType>,
2022-08-09 03:26:21 +00:00
conversationIds: Array<string>,
2022-08-04 19:23:24 +00:00
attachment: AttachmentType
): ThunkAction<
void,
RootStateType,
unknown,
SendStoryModalOpenStateChanged | SetStorySendingType | SetAddStoryDataType
> {
return async (dispatch, getState) => {
const { stories } = getState();
const { openedAtTimestamp, sendStoryModalData } = stories;
// Add spinners in the story creator
dispatch({
type: SET_STORY_SENDING,
payload: true,
});
assertDev(
openedAtTimestamp,
'sendStoryMessage: openedAtTimestamp is undefined, cannot send'
);
assertDev(
sendStoryModalData,
'sendStoryMessage: sendStoryModalData is not defined, cannot send'
);
2022-08-02 19:31:55 +00:00
if (sendStoryModalData.untrustedUuids.length) {
log.info('sendStoryMessage: SN changed for some conversations');
const conversationsNeedingVerification: Array<ConversationModel> =
sendStoryModalData.untrustedUuids
.map(uuid => window.ConversationController.get(uuid))
.filter(isNotNil);
if (!conversationsNeedingVerification.length) {
log.warn(
'sendStoryMessage: Could not retrieve conversations for untrusted uuids'
);
return;
}
const result = await blockSendUntilConversationsAreVerified(
conversationsNeedingVerification,
SafetyNumberChangeSource.Story,
Date.now() - openedAtTimestamp
);
if (!result) {
log.info('sendStoryMessage: failed to verify untrusted; stopping send');
dispatch({
type: SET_STORY_SENDING,
payload: false,
});
return;
}
// Clear all untrusted and verified uuids; we're clear to send!
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: undefined,
});
}
try {
await doSendStoryMessage(listIds, conversationIds, attachment);
// Note: Only when we've successfully queued the message do we dismiss the story
// composer view.
dispatch({
type: SET_ADD_STORY_DATA,
payload: undefined,
});
} catch (error) {
log.error('sendStoryMessage:', Errors.toLogFormat(error));
// Get rid of spinners in the story creator
dispatch({
type: SET_STORY_SENDING,
payload: false,
});
}
2022-08-02 19:31:55 +00:00
};
}
2022-03-29 01:10:08 +00:00
function storyChanged(story: StoryDataType): StoryChangedActionType {
return {
type: STORY_CHANGED,
payload: story,
};
}
function sendStoryModalOpenStateChanged(
value: boolean
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
return (dispatch, getState) => {
const { stories } = getState();
if (!stories.sendStoryModalData && value) {
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: Date.now(),
});
}
if (stories.sendStoryModalData && !value) {
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: undefined,
});
}
};
}
2022-03-04 21:14:52 +00:00
function toggleStoriesView(): ToggleViewActionType {
return {
type: TOGGLE_VIEW,
};
}
function verifyStoryListMembers(
memberUuids: Array<string>
): ThunkAction<void, RootStateType, unknown, ListMembersVerified> {
return async (dispatch, getState) => {
const { stories } = getState();
const { sendStoryModalData } = stories;
if (!sendStoryModalData) {
return;
}
const alreadyVerifiedUuids = new Set([...sendStoryModalData.verifiedUuids]);
const uuidsNeedingVerification = memberUuids.filter(
uuid => !alreadyVerifiedUuids.has(uuid)
);
if (!uuidsNeedingVerification.length) {
return;
}
const { untrustedUuids, verifiedUuids } = await doVerifyStoryListMembers(
uuidsNeedingVerification
);
dispatch({
type: LIST_MEMBERS_VERIFIED,
payload: {
untrustedUuids: Array.from(untrustedUuids),
verifiedUuids: Array.from(verifiedUuids),
},
});
};
}
2022-10-17 16:33:07 +00:00
const getSelectedStoryDataForDistributionListId = (
getState: () => RootStateType,
distributionListId: string | undefined,
selectedStoryId?: string
): {
currentIndex: number;
numStories: number;
storiesByConversationId: Array<StoryDataType>;
} => {
const state = getState();
const { stories } = state.stories;
const storiesByDistributionList = stories.filter(
item =>
item.storyDistributionListId === distributionListId &&
!item.deletedForEveryone
);
const numStories = storiesByDistributionList.length;
const currentIndex = selectedStoryId
? storiesByDistributionList.findIndex(
item => item.messageId === selectedStoryId
)
: 0;
return {
currentIndex,
numStories,
storiesByConversationId: [],
};
};
2022-07-06 19:06:20 +00:00
const getSelectedStoryDataForConversationId = (
dispatch: ThunkDispatch<RootStateType, unknown, NoopActionType>,
2022-07-06 19:06:20 +00:00
getState: () => RootStateType,
conversationId: string,
selectedStoryId?: string
): {
currentIndex: number;
hasUnread: boolean;
numStories: number;
storiesByConversationId: Array<StoryDataType>;
} => {
const state = getState();
const { stories } = state.stories;
const storiesByConversationId = stories.filter(
2022-07-29 18:12:47 +00:00
item => item.conversationId === conversationId && !item.deletedForEveryone
2022-07-06 19:06:20 +00:00
);
// Find the index of the storyId provided, or if none provided then find the
2022-10-17 16:33:07 +00:00
// oldest unviewed story from the user. If all stories are read then we can
2022-07-06 19:06:20 +00:00
// start at the first story.
let currentIndex: number | undefined;
2022-07-06 19:06:20 +00:00
let hasUnread = false;
storiesByConversationId.forEach((item, index) => {
if (selectedStoryId && item.messageId === selectedStoryId) {
currentIndex = index;
}
if (
!selectedStoryId &&
currentIndex === undefined &&
2022-07-06 19:06:20 +00:00
item.readStatus === ReadStatus.Unread
) {
hasUnread = true;
currentIndex = index;
}
});
const numStories = storiesByConversationId.length;
// Queue all undownloaded stories once we're viewing someone's stories
storiesByConversationId.forEach(({ attachment, messageId }) => {
if (isDownloaded(attachment) || isDownloading(attachment)) {
2022-07-06 19:06:20 +00:00
return;
}
queueStoryDownload(messageId)(dispatch, getState, null);
2022-07-06 19:06:20 +00:00
});
return {
currentIndex: currentIndex ?? 0,
2022-07-06 19:06:20 +00:00
hasUnread,
numStories,
storiesByConversationId,
};
};
2022-08-22 17:44:23 +00:00
export type ViewUserStoriesActionCreatorType = (opts: {
conversationId: string;
storyViewMode?: StoryViewModeType;
2022-10-17 16:33:07 +00:00
viewTarget?: StoryViewTargetType;
2022-08-22 17:44:23 +00:00
}) => unknown;
const viewUserStories: ViewUserStoriesActionCreatorType = ({
conversationId,
storyViewMode,
2022-10-17 16:33:07 +00:00
viewTarget,
2022-08-22 17:44:23 +00:00
}): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
2022-07-06 19:06:20 +00:00
return (dispatch, getState) => {
const { currentIndex, hasUnread, numStories, storiesByConversationId } =
getSelectedStoryDataForConversationId(dispatch, getState, conversationId);
const story = storiesByConversationId[currentIndex];
2022-10-17 16:33:07 +00:00
const state = getState();
2022-07-06 19:06:20 +00:00
2022-10-17 16:33:07 +00:00
const hiddenConversationIds = new Set(getHideStoryConversationIds(state));
2022-09-22 18:56:39 +00:00
let inferredStoryViewMode: StoryViewModeType;
if (storyViewMode) {
inferredStoryViewMode = storyViewMode;
} else if (hiddenConversationIds.has(conversationId)) {
inferredStoryViewMode = StoryViewModeType.Hidden;
} else if (hasUnread) {
inferredStoryViewMode = StoryViewModeType.Unread;
} else {
inferredStoryViewMode = StoryViewModeType.All;
}
2022-10-17 16:33:07 +00:00
let unviewedStoryConversationIdsSorted: Array<string> = [];
if (
inferredStoryViewMode === StoryViewModeType.Unread ||
inferredStoryViewMode === StoryViewModeType.Hidden
) {
const storiesSelectorState = getStories(state);
const conversationStories =
inferredStoryViewMode === StoryViewModeType.Hidden
? storiesSelectorState.hiddenStories
: storiesSelectorState.stories;
unviewedStoryConversationIdsSorted = conversationStories
.filter(item => item.storyView.isUnread)
.map(item => item.conversationId);
}
2022-07-06 19:06:20 +00:00
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex,
messageId: story.messageId,
numStories,
2022-09-22 18:56:39 +00:00
storyViewMode: inferredStoryViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
viewTarget,
2022-07-06 19:06:20 +00:00
},
});
};
2022-08-22 17:44:23 +00:00
};
2022-07-06 19:06:20 +00:00
function removeAllStories(): RemoveAllStoriesActionType {
return {
type: REMOVE_ALL_STORIES,
};
}
2022-09-22 00:55:23 +00:00
type ViewStoryOptionsType =
| {
closeViewer: true;
}
| {
storyId: string;
storyViewMode: StoryViewModeType;
viewDirection?: StoryViewDirectionType;
2022-10-11 17:59:02 +00:00
viewTarget?: StoryViewTargetType;
2022-09-22 00:55:23 +00:00
};
2022-08-22 17:44:23 +00:00
export type ViewStoryActionCreatorType = (
2022-09-22 00:55:23 +00:00
opts: ViewStoryOptionsType
2022-08-22 17:44:23 +00:00
) => unknown;
2022-07-25 18:55:44 +00:00
2022-09-22 00:55:23 +00:00
export type DispatchableViewStoryType = (
opts: ViewStoryOptionsType
) => ThunkAction<void, RootStateType, unknown, ViewStoryActionType>;
2022-08-22 17:44:23 +00:00
const viewStory: ViewStoryActionCreatorType = (
opts
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
2022-07-06 19:06:20 +00:00
return (dispatch, getState) => {
2022-08-22 17:44:23 +00:00
if ('closeViewer' in opts) {
2022-07-06 19:06:20 +00:00
dispatch({
type: VIEW_STORY,
payload: undefined,
});
return;
}
2022-10-11 17:59:02 +00:00
const { viewTarget, storyId, storyViewMode, viewDirection } = opts;
2022-08-22 17:44:23 +00:00
2022-07-06 19:06:20 +00:00
const state = getState();
2022-10-17 16:33:07 +00:00
const { selectedStoryData, stories } = state.stories;
const unviewedStoryConversationIdsSorted =
selectedStoryData?.unviewedStoryConversationIdsSorted || [];
2022-07-06 19:06:20 +00:00
// Spec:
// When opening the story viewer you should always be taken to the oldest
// un viewed story of the user you tapped on
// If all stories from a user are viewed, opening the viewer should take
// you to their oldest story
2022-07-29 18:12:47 +00:00
const story = stories.find(
item => item.messageId === storyId && !item.deletedForEveryone
);
2022-07-06 19:06:20 +00:00
if (!story) {
2022-08-22 17:44:23 +00:00
log.warn('stories.viewStory: No story found', storyId);
dispatch({
type: VIEW_STORY,
payload: undefined,
});
2022-07-06 19:06:20 +00:00
return;
}
const { currentIndex, numStories, storiesByConversationId } =
storyViewMode === StoryViewModeType.MyStories &&
story.storyDistributionListId
2022-10-17 16:33:07 +00:00
? getSelectedStoryDataForDistributionListId(
getState,
story.storyDistributionListId,
storyId
)
: getSelectedStoryDataForConversationId(
dispatch,
getState,
story.conversationId,
storyId
);
2022-07-06 19:06:20 +00:00
// Go directly to the storyId selected
if (!viewDirection) {
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex,
messageId: storyId,
numStories,
2022-10-17 16:33:07 +00:00
storyViewMode,
unviewedStoryConversationIdsSorted,
2022-10-11 17:59:02 +00:00
viewTarget,
2022-10-17 16:33:07 +00:00
},
});
return;
}
if (storyViewMode === StoryViewModeType.Single) {
// Close the viewer we were just looking at a single story.
dispatch({
type: VIEW_STORY,
payload: undefined,
});
return;
}
2022-10-17 16:33:07 +00:00
// When paging through all sent stories
// Note the order is reversed[1][2] here because we sort the stories by
// recency in descending order but the story viewer plays them in
// ascending order.
if (storyViewMode === StoryViewModeType.MyStories) {
const { myStories } = getStories(state);
let currentStoryIndex = -1;
const currentDistributionListIndex = myStories.findIndex(item => {
for (let i = item.stories.length - 1; i >= 0; i -= 1) {
const myStory = item.stories[i];
if (myStory.messageId === storyId) {
// [1] reversed
currentStoryIndex = item.stories.length - 1 - i;
return true;
}
}
return false;
});
if (currentDistributionListIndex < 0 || currentStoryIndex < 0) {
log.warn('stories.viewStory: No current story found for MyStories', {
currentDistributionListIndex,
currentStoryIndex,
myStories: myStories.length,
});
dispatch({
type: VIEW_STORY,
payload: undefined,
});
return;
}
let nextSentStoryId: string | undefined;
let nextSentStoryIndex = -1;
let nextNumStories = numStories;
// [2] reversed
const currentStories = myStories[currentDistributionListIndex].stories
.slice()
.reverse();
if (viewDirection === StoryViewDirectionType.Next) {
if (currentStoryIndex < currentStories.length - 1) {
nextSentStoryIndex = currentStoryIndex + 1;
nextSentStoryId = currentStories[nextSentStoryIndex].messageId;
} else if (currentDistributionListIndex < myStories.length - 1) {
const nextSentStoryContainer =
myStories[currentDistributionListIndex + 1];
nextNumStories = nextSentStoryContainer.stories.length;
nextSentStoryIndex = 0;
nextSentStoryId =
nextSentStoryContainer.stories[nextNumStories - 1].messageId;
}
}
if (viewDirection === StoryViewDirectionType.Previous) {
if (currentStoryIndex > 0) {
nextSentStoryIndex = currentStoryIndex - 1;
nextSentStoryId = currentStories[nextSentStoryIndex].messageId;
} else if (currentDistributionListIndex > 0) {
const nextSentStoryContainer =
myStories[currentDistributionListIndex - 1];
nextNumStories = nextSentStoryContainer.stories.length;
nextSentStoryIndex = nextNumStories - 1;
nextSentStoryId = nextSentStoryContainer.stories[0].messageId;
}
}
if (!nextSentStoryId) {
dispatch({
type: VIEW_STORY,
payload: undefined,
});
return;
}
dispatch({
type: VIEW_STORY,
payload: {
currentIndex: nextSentStoryIndex,
messageId: nextSentStoryId,
numStories: nextNumStories,
2022-07-06 19:06:20 +00:00
storyViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
2022-07-06 19:06:20 +00:00
},
});
return;
}
// Next story within the same user's stories
if (
viewDirection === StoryViewDirectionType.Next &&
currentIndex < numStories - 1
) {
const nextIndex = currentIndex + 1;
const nextStory = storiesByConversationId[nextIndex];
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex: nextIndex,
messageId: nextStory.messageId,
numStories,
2022-07-06 19:06:20 +00:00
storyViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
2022-07-06 19:06:20 +00:00
},
});
return;
}
// Prev story within the same user's stories
if (viewDirection === StoryViewDirectionType.Previous && currentIndex > 0) {
const nextIndex = currentIndex - 1;
const nextStory = storiesByConversationId[nextIndex];
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex: nextIndex,
messageId: nextStory.messageId,
numStories,
2022-07-06 19:06:20 +00:00
storyViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
2022-07-06 19:06:20 +00:00
},
});
return;
}
2022-08-22 17:44:23 +00:00
// We were just viewing a single user's stories. Close the viewer.
if (storyViewMode === StoryViewModeType.User) {
dispatch({
type: VIEW_STORY,
payload: undefined,
});
return;
}
const storiesSelectorState = getStories(state);
const conversationStories =
storyViewMode === StoryViewModeType.Hidden
? storiesSelectorState.hiddenStories
: storiesSelectorState.stories;
const conversationStoryIndex = conversationStories.findIndex(
item => item.conversationId === story.conversationId
);
2022-07-06 19:06:20 +00:00
// Are there any unviewed stories left? If so we should play the unviewed
2022-10-17 16:33:07 +00:00
// stories first.
if (storyViewMode === StoryViewModeType.Unread) {
const frozenConversationStoryIndex =
unviewedStoryConversationIdsSorted.findIndex(
conversationId => conversationId === story.conversationId
);
2022-09-22 00:55:23 +00:00
2022-10-17 16:33:07 +00:00
let nextUnreadConversationId: string | undefined;
if (viewDirection === StoryViewDirectionType.Previous) {
nextUnreadConversationId =
unviewedStoryConversationIdsSorted[frozenConversationStoryIndex - 1];
} else if (viewDirection === StoryViewDirectionType.Next) {
nextUnreadConversationId =
unviewedStoryConversationIdsSorted[frozenConversationStoryIndex + 1];
}
if (nextUnreadConversationId) {
2022-07-06 19:06:20 +00:00
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
dispatch,
getState,
2022-10-17 16:33:07 +00:00
nextUnreadConversationId
2022-07-06 19:06:20 +00:00
);
2022-07-06 19:06:20 +00:00
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex: nextSelectedStoryData.currentIndex,
messageId:
nextSelectedStoryData.storiesByConversationId[
nextSelectedStoryData.currentIndex
].messageId,
2022-08-22 17:44:23 +00:00
numStories: nextSelectedStoryData.numStories,
2022-07-06 19:06:20 +00:00
storyViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
2022-07-06 19:06:20 +00:00
},
});
return;
}
2022-09-22 00:55:23 +00:00
2022-10-17 16:33:07 +00:00
// Close the viewer if we were viewing unviewed stories only and we did
// not find any more unviewed.
dispatch({
type: VIEW_STORY,
payload: undefined,
});
return;
2022-07-06 19:06:20 +00:00
}
if (conversationStoryIndex < 0) {
2022-08-22 17:44:23 +00:00
log.warn('stories.viewStory: No stories found for conversation', {
storiesLength: conversationStories.length,
});
dispatch({
type: VIEW_STORY,
payload: undefined,
});
2022-07-06 19:06:20 +00:00
return;
}
// Find the next user's stories
if (
viewDirection === StoryViewDirectionType.Next &&
conversationStoryIndex < conversationStories.length - 1
) {
// Spec:
// Tapping right advances you to the next un viewed story
// If all stories are viewed, advance to the next viewed story
// When you reach the newest story from a user, tapping right again
// should take you to the next user's oldest un viewed story or oldest
// story if all stories for the next user are viewed.
// When you reach the newest story from the last user in the story list,
// tapping right should close the viewer
// Touch area for tapping right should be 80% of width of the screen
const nextConversationStoryIndex = conversationStoryIndex + 1;
const conversationStory = conversationStories[nextConversationStoryIndex];
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
dispatch,
getState,
conversationStory.conversationId
);
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex: 0,
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
numStories: nextSelectedStoryData.numStories,
2022-07-06 19:06:20 +00:00
storyViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
2022-07-06 19:06:20 +00:00
},
});
return;
}
// Find the previous user's stories
if (
viewDirection === StoryViewDirectionType.Previous &&
conversationStoryIndex > 0
) {
// Spec:
// Tapping left takes you back to the previous story
// When you reach the oldest story from a user, tapping left again takes
// you to the previous users oldest un viewed story or newest viewed
// story if all stories are viewed
// If you tap left on the oldest story from the first user in the story
// list, it should re-start playback on that story
// Touch area for tapping left should be 20% of width of the screen
const nextConversationStoryIndex = conversationStoryIndex - 1;
const conversationStory = conversationStories[nextConversationStoryIndex];
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
dispatch,
getState,
conversationStory.conversationId
);
dispatch({
type: VIEW_STORY,
payload: {
2022-08-22 17:44:23 +00:00
currentIndex: 0,
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
numStories: nextSelectedStoryData.numStories,
2022-07-06 19:06:20 +00:00
storyViewMode,
2022-10-17 16:33:07 +00:00
unviewedStoryConversationIdsSorted,
2022-07-06 19:06:20 +00:00
},
});
return;
}
// Could not meet any criteria, close the viewer
dispatch({
type: VIEW_STORY,
payload: undefined,
});
};
};
function setAddStoryData(addStoryData: AddStoryData): SetAddStoryDataType {
return {
type: SET_ADD_STORY_DATA,
payload: addStoryData,
};
}
function setStorySending(sending: boolean): SetStorySendingType {
return {
type: SET_STORY_SENDING,
payload: sending,
};
}
function setStoriesDisabled(
value: boolean
): ThunkAction<void, RootStateType, unknown, never> {
return async () => {
await window.Events.setHasStoriesDisabled(value);
};
}
2022-07-06 19:06:20 +00:00
export const actions = {
deleteStoryForEveryone,
loadStoryReplies,
markStoryRead,
queueStoryDownload,
reactToStory,
removeAllStories,
2022-07-06 19:06:20 +00:00
replyToStory,
2022-08-02 19:31:55 +00:00
sendStoryMessage,
sendStoryModalOpenStateChanged,
2022-07-06 19:06:20 +00:00
storyChanged,
toggleStoriesView,
verifyStoryListMembers,
2022-07-06 19:06:20 +00:00
viewUserStories,
viewStory,
2022-11-04 13:22:07 +00:00
deleteGroupStoryReply,
deleteGroupStoryReplyForEveryone,
setAddStoryData,
setStoriesDisabled,
setStorySending,
2022-07-06 19:06:20 +00:00
};
export const useStoriesActions = (): typeof actions => useBoundActions(actions);
2022-03-04 21:14:52 +00:00
// Reducer
export function getEmptyState(
overrideState: Partial<StoriesStateType> = {}
): StoriesStateType {
return {
lastOpenedAtTimestamp: undefined,
openedAtTimestamp: undefined,
addStoryData: undefined,
2022-03-04 21:14:52 +00:00
stories: [],
...overrideState,
};
}
export function reducer(
state: Readonly<StoriesStateType> = getEmptyState(),
action: Readonly<StoriesActionType>
): StoriesStateType {
if (action.type === TOGGLE_VIEW) {
const isShowingStoriesView = Boolean(state.openedAtTimestamp);
2022-03-04 21:14:52 +00:00
return {
...state,
lastOpenedAtTimestamp: !isShowingStoriesView
? state.openedAtTimestamp || Date.now()
: state.lastOpenedAtTimestamp,
openedAtTimestamp: isShowingStoriesView ? undefined : Date.now(),
replyState: undefined,
sendStoryModalData: undefined,
selectedStoryData: isShowingStoriesView
2022-07-22 01:38:27 +00:00
? undefined
: state.selectedStoryData,
2022-03-04 21:14:52 +00:00
};
}
2022-11-04 13:22:07 +00:00
if (action.type === STORY_REPLY_DELETED) {
return {
...state,
replyState: state.replyState
? {
...state.replyState,
replies: state.replyState.replies.filter(
reply => reply.id !== action.payload
),
}
: undefined,
};
}
2022-03-04 21:14:52 +00:00
if (action.type === 'MESSAGE_DELETED') {
2022-04-15 00:08:46 +00:00
const nextStories = state.stories.filter(
story => story.messageId !== action.payload.id
);
if (nextStories.length === state.stories.length) {
return state;
}
2022-03-04 21:14:52 +00:00
return {
...state,
2022-04-15 00:08:46 +00:00
stories: nextStories,
2022-03-04 21:14:52 +00:00
};
}
2022-03-29 01:10:08 +00:00
if (action.type === STORY_CHANGED) {
2022-03-04 21:14:52 +00:00
const newStory = pick(action.payload, [
'attachment',
2022-07-01 00:52:03 +00:00
'canReplyToStory',
2022-03-04 21:14:52 +00:00
'conversationId',
2022-04-15 00:08:46 +00:00
'deletedForEveryone',
'expirationStartTimestamp',
'expireTimer',
'hasReplies',
'hasRepliesFromSelf',
2022-03-04 21:14:52 +00:00
'messageId',
2022-04-28 22:06:28 +00:00
'reactions',
2022-09-21 23:54:48 +00:00
'readAt',
2022-03-04 21:14:52 +00:00
'readStatus',
2022-04-15 00:08:46 +00:00
'sendStateByConversationId',
2022-03-04 21:14:52 +00:00
'source',
'sourceUuid',
2022-07-01 00:52:03 +00:00
'storyDistributionListId',
2022-03-04 21:14:52 +00:00
'timestamp',
2022-04-15 00:08:46 +00:00
'type',
2022-03-04 21:14:52 +00:00
]);
2022-04-21 00:29:37 +00:00
const prevStoryIndex = state.stories.findIndex(
2022-03-04 21:14:52 +00:00
existingStory => existingStory.messageId === newStory.messageId
);
2022-04-21 00:29:37 +00:00
if (prevStoryIndex >= 0) {
const prevStory = state.stories[prevStoryIndex];
2022-03-29 01:10:08 +00:00
2022-04-21 00:29:37 +00:00
// Stories rarely need to change, here are the following exceptions:
const isDownloadingAttachment = isDownloading(newStory.attachment);
const hasAttachmentDownloaded =
!isDownloaded(prevStory.attachment) &&
isDownloaded(newStory.attachment);
2022-08-04 00:38:41 +00:00
const hasAttachmentFailed =
hasFailed(newStory.attachment) && !hasFailed(prevStory.attachment);
2022-10-17 23:30:03 +00:00
const hasExpirationChanged =
(newStory.expirationStartTimestamp &&
!prevStory.expirationStartTimestamp) ||
(newStory.expireTimer && !prevStory.expireTimer);
2022-04-21 00:29:37 +00:00
const readStatusChanged = prevStory.readStatus !== newStory.readStatus;
2022-04-28 22:06:28 +00:00
const reactionsChanged =
prevStory.reactions?.length !== newStory.reactions?.length;
2022-07-01 00:52:03 +00:00
const hasBeenDeleted =
!prevStory.deletedForEveryone && newStory.deletedForEveryone;
const hasSendStateChanged = !isEqual(
prevStory.sendStateByConversationId,
newStory.sendStateByConversationId
);
2022-03-29 01:10:08 +00:00
2022-04-21 00:29:37 +00:00
const shouldReplace =
2022-04-28 22:06:28 +00:00
isDownloadingAttachment ||
hasAttachmentDownloaded ||
2022-08-04 00:38:41 +00:00
hasAttachmentFailed ||
2022-07-01 00:52:03 +00:00
hasBeenDeleted ||
2022-10-17 23:30:03 +00:00
hasExpirationChanged ||
2022-07-01 00:52:03 +00:00
hasSendStateChanged ||
2022-04-28 22:06:28 +00:00
readStatusChanged ||
reactionsChanged;
2022-04-21 00:29:37 +00:00
if (!shouldReplace) {
2022-04-15 00:08:46 +00:00
return state;
}
2022-07-29 18:12:47 +00:00
if (hasBeenDeleted) {
return {
...state,
stories: state.stories.filter(
existingStory => existingStory.messageId !== newStory.messageId
),
};
}
2022-03-29 01:10:08 +00:00
return {
...state,
2022-04-21 00:29:37 +00:00
stories: replaceIndex(state.stories, prevStoryIndex, newStory),
2022-03-29 01:10:08 +00:00
};
2022-03-04 21:14:52 +00:00
}
2022-04-21 00:29:37 +00:00
// Adding a new story
2022-03-29 01:10:08 +00:00
const stories = [...state.stories, newStory].sort((a, b) =>
a.timestamp > b.timestamp ? 1 : -1
2022-03-04 21:14:52 +00:00
);
return {
...state,
stories,
};
}
2022-03-29 01:10:08 +00:00
if (action.type === MARK_STORY_READ) {
2022-09-21 23:54:48 +00:00
const { messageId, readAt } = action.payload;
2022-03-29 01:10:08 +00:00
return {
...state,
stories: state.stories.map(story => {
2022-09-21 23:54:48 +00:00
if (story.messageId === messageId) {
2022-03-29 01:10:08 +00:00
return {
...story,
2022-09-21 23:54:48 +00:00
readAt,
2022-03-29 01:10:08 +00:00
readStatus: ReadStatus.Viewed,
};
}
return story;
}),
};
}
2022-04-15 00:08:46 +00:00
if (action.type === LOAD_STORY_REPLIES) {
return {
...state,
stories: state.stories.map(story => {
if (story.messageId === action.payload.messageId) {
return {
...story,
hasReplies: action.payload.replies.length > 0,
hasRepliesFromSelf: action.payload.replies.some(
reply => reply.type === 'outgoing'
),
};
}
return story;
}),
2022-04-15 00:08:46 +00:00
replyState: action.payload,
};
}
2022-08-02 19:31:55 +00:00
if (action.type === 'MESSAGES_ADDED' && action.payload.isJustSent) {
const stories = action.payload.messages.filter(isStory);
if (!stories.length) {
return state;
}
const newStories = stories
.map(messageAttrs => getStoryDataFromMessageAttributes(messageAttrs))
.filter(isNotNil);
if (!newStories.length) {
return state;
}
return {
...state,
stories: [...state.stories, ...newStories],
};
}
2022-04-15 00:08:46 +00:00
// 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) {
const storyIndex = state.stories.findIndex(
story => story.messageId === replyState.messageId
);
const stories =
storyIndex >= 0
? replaceIndex(state.stories, storyIndex, {
...state.stories[storyIndex],
hasReplies: true,
hasRepliesFromSelf:
state.stories[storyIndex].hasRepliesFromSelf ||
state.stories[storyIndex].conversationId ===
action.payload.conversationId,
})
: state.stories;
2022-04-15 00:08:46 +00:00
return {
...state,
stories,
2022-04-15 00:08:46 +00:00
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
),
},
};
}
2022-07-01 00:52:03 +00:00
if (action.type === DOE_STORY) {
return {
...state,
2022-07-29 18:12:47 +00:00
stories: state.stories.filter(
existingStory => existingStory.messageId !== action.payload
),
2022-07-01 00:52:03 +00:00
};
}
2022-07-06 19:06:20 +00:00
if (action.type === VIEW_STORY) {
return {
...state,
2022-08-22 17:44:23 +00:00
selectedStoryData: action.payload,
2022-07-06 19:06:20 +00:00
};
}
if (action.type === REMOVE_ALL_STORIES) {
return getEmptyState();
}
if (action.type === QUEUE_STORY_DOWNLOAD) {
const storyIndex = state.stories.findIndex(
story => story.messageId === action.payload
);
if (storyIndex < 0) {
return state;
}
const existingStory = state.stories[storyIndex];
return {
...state,
stories: replaceIndex(state.stories, storyIndex, {
...existingStory,
startedDownload: true,
}),
};
}
if (action.type === SEND_STORY_MODAL_OPEN_STATE_CHANGED) {
if (action.payload) {
return {
...state,
sendStoryModalData: {
untrustedUuids: [],
verifiedUuids: [],
},
};
}
return {
...state,
sendStoryModalData: undefined,
};
}
if (action.type === LIST_MEMBERS_VERIFIED) {
const sendStoryModalData = {
untrustedUuids: [],
verifiedUuids: [],
...(state.sendStoryModalData || {}),
};
const untrustedUuids = Array.from(
new Set([
...sendStoryModalData.untrustedUuids,
...action.payload.untrustedUuids,
])
);
const verifiedUuids = Array.from(
new Set([
...sendStoryModalData.verifiedUuids,
...action.payload.verifiedUuids,
])
);
return {
...state,
sendStoryModalData: {
...sendStoryModalData,
untrustedUuids,
verifiedUuids,
},
};
}
if (action.type === SET_ADD_STORY_DATA) {
return {
...state,
addStoryData: action.payload,
};
}
if (action.type === SET_STORY_SENDING) {
const existing = state.addStoryData;
if (!existing) {
log.warn(
'stories/reducer: Set story sending, but no existing addStoryData'
);
return state;
}
return {
...state,
addStoryData: {
...existing,
sending: action.payload,
},
};
}
2022-03-04 21:14:52 +00:00
return state;
}