2022-03-04 21:14:52 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2022-07-06 19:06:20 +00:00
|
|
|
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
2022-07-13 23:09:18 +00:00
|
|
|
import { isEqual, noop, pick } from 'lodash';
|
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 { 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-07-01 00:52:03 +00:00
|
|
|
import type { 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-07-01 00:52:03 +00:00
|
|
|
import { DAY } from '../../util/durations';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
2022-07-06 19:06:20 +00:00
|
|
|
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
2022-07-13 23:09:18 +00:00
|
|
|
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
|
|
|
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-07-01 00:52:03 +00:00
|
|
|
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { showToast } from '../../util/showToast';
|
2022-04-25 17:25:50 +00:00
|
|
|
import {
|
2022-08-04 00:38:41 +00:00
|
|
|
hasFailed,
|
2022-04-25 17:25:50 +00:00
|
|
|
hasNotResolved,
|
|
|
|
isDownloaded,
|
|
|
|
isDownloading,
|
|
|
|
} from '../../types/Attachment';
|
2022-07-06 19:06:20 +00:00
|
|
|
import { getConversationSelector } from '../selectors/conversations';
|
2022-07-13 23:09:18 +00:00
|
|
|
import { getSendOptions } from '../../util/getSendOptions';
|
2022-07-06 19:06:20 +00:00
|
|
|
import { getStories } 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';
|
2022-07-13 23:09:18 +00:00
|
|
|
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
|
2022-08-02 19:31:55 +00:00
|
|
|
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
|
|
|
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
|
|
|
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
|
|
|
|
|
|
|
export type StoryDataType = {
|
|
|
|
attachment?: AttachmentType;
|
|
|
|
messageId: string;
|
|
|
|
} & 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-04-15 00:08:46 +00:00
|
|
|
| 'readStatus'
|
|
|
|
| 'sendStateByConversationId'
|
|
|
|
| 'source'
|
|
|
|
| 'sourceUuid'
|
2022-07-01 00:52:03 +00:00
|
|
|
| 'storyDistributionListId'
|
2022-04-15 00:08:46 +00:00
|
|
|
| 'timestamp'
|
|
|
|
| 'type'
|
2022-03-04 21:14:52 +00:00
|
|
|
>;
|
|
|
|
|
2022-07-06 19:06:20 +00:00
|
|
|
export type SelectedStoryDataType = {
|
|
|
|
currentIndex: number;
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId: string;
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories: number;
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal: boolean;
|
2022-07-06 19:06:20 +00:00
|
|
|
};
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
// State
|
|
|
|
|
|
|
|
export type StoriesStateType = {
|
|
|
|
readonly isShowingStoriesView: boolean;
|
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;
|
2022-03-04 21:14:52 +00:00
|
|
|
readonly stories: Array<StoryDataType>;
|
2022-07-06 19:06:20 +00:00
|
|
|
readonly storyViewMode?: StoryViewModeType;
|
2022-03-04 21:14:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
2022-07-01 00:52:03 +00:00
|
|
|
const DOE_STORY = 'stories/DOE';
|
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';
|
2022-04-15 00:08:46 +00:00
|
|
|
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
|
2022-04-25 17:25:50 +00:00
|
|
|
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
|
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-03-04 21:14:52 +00:00
|
|
|
|
2022-07-01 00:52:03 +00:00
|
|
|
type DOEStoryActionType = {
|
|
|
|
type: typeof DOE_STORY;
|
|
|
|
payload: 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;
|
|
|
|
payload: string;
|
2022-03-04 21:14:52 +00:00
|
|
|
};
|
|
|
|
|
2022-04-15 00:08:46 +00:00
|
|
|
type ReplyToStoryActionType = {
|
|
|
|
type: typeof REPLY_TO_STORY;
|
|
|
|
payload: MessageAttributesType;
|
|
|
|
};
|
|
|
|
|
2022-04-25 17:25:50 +00:00
|
|
|
type ResolveAttachmentUrlActionType = {
|
|
|
|
type: typeof RESOLVE_ATTACHMENT_URL;
|
|
|
|
payload: {
|
|
|
|
messageId: string;
|
|
|
|
attachmentUrl: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
payload:
|
|
|
|
| {
|
|
|
|
selectedStoryData: SelectedStoryDataType;
|
|
|
|
storyViewMode: StoryViewModeType;
|
|
|
|
}
|
|
|
|
| undefined;
|
|
|
|
};
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
export type StoriesActionType =
|
2022-07-01 00:52:03 +00:00
|
|
|
| DOEStoryActionType
|
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
|
2022-04-15 00:08:46 +00:00
|
|
|
| ReplyToStoryActionType
|
2022-04-25 17:25:50 +00:00
|
|
|
| ResolveAttachmentUrlActionType
|
2022-03-29 01:10:08 +00:00
|
|
|
| StoryChangedActionType
|
2022-07-06 19:06:20 +00:00
|
|
|
| ToggleViewActionType
|
|
|
|
| ViewStoryActionType;
|
2022-03-04 21:14:52 +00:00
|
|
|
|
|
|
|
// Action Creators
|
|
|
|
|
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 conversationIds = new Set(
|
|
|
|
story.sendState.map(({ recipient }) => recipient.id)
|
|
|
|
);
|
2022-07-13 23:09:18 +00:00
|
|
|
const updatedStoryRecipients = new Map<
|
|
|
|
string,
|
|
|
|
{
|
|
|
|
distributionListIds: Set<string>;
|
|
|
|
isAllowedToReply: boolean;
|
|
|
|
}
|
|
|
|
>();
|
|
|
|
|
|
|
|
const ourConversation =
|
|
|
|
window.ConversationController.getOurConversationOrThrow();
|
|
|
|
|
|
|
|
// Remove ourselves from the DOE.
|
|
|
|
conversationIds.delete(ourConversation.id);
|
2022-07-01 00:52:03 +00:00
|
|
|
|
|
|
|
// Find stories that were sent to other distribution lists so that we don't
|
|
|
|
// send a DOE request to the members of those lists.
|
|
|
|
const { stories } = getState().stories;
|
|
|
|
stories.forEach(item => {
|
2022-07-13 23:09:18 +00:00
|
|
|
const { sendStateByConversationId } = item;
|
|
|
|
// We only want matching timestamp stories which are stories that were
|
|
|
|
// sent to multi distribution lists.
|
|
|
|
// We don't want the story we just passed in.
|
|
|
|
// Don't need to check for stories that have already been deleted.
|
|
|
|
// And only for sent stories, not incoming.
|
|
|
|
if (
|
|
|
|
item.timestamp !== story.timestamp ||
|
|
|
|
item.messageId === story.messageId ||
|
|
|
|
item.deletedForEveryone ||
|
|
|
|
!sendStateByConversationId
|
|
|
|
) {
|
2022-07-01 00:52:03 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-13 23:09:18 +00:00
|
|
|
Object.keys(sendStateByConversationId).forEach(conversationId => {
|
|
|
|
if (conversationId === ourConversation.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const destinationUuid =
|
|
|
|
window.ConversationController.get(conversationId)?.get('uuid');
|
|
|
|
|
|
|
|
if (!destinationUuid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const distributionListIds =
|
|
|
|
updatedStoryRecipients.get(destinationUuid)?.distributionListIds ||
|
|
|
|
new Set();
|
2022-07-01 00:52:03 +00:00
|
|
|
|
2022-07-13 23:09:18 +00:00
|
|
|
// These are the remaining distribution list ids that the user has
|
|
|
|
// access to.
|
|
|
|
updatedStoryRecipients.set(destinationUuid, {
|
|
|
|
distributionListIds: item.storyDistributionListId
|
|
|
|
? new Set([...distributionListIds, item.storyDistributionListId])
|
|
|
|
: distributionListIds,
|
|
|
|
isAllowedToReply:
|
|
|
|
sendStateByConversationId[conversationId]
|
|
|
|
.isAllowedToReplyToStory !== false,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Remove this conversationId so we don't send the DOE to those that
|
|
|
|
// still have access.
|
2022-07-01 00:52:03 +00:00
|
|
|
conversationIds.delete(conversationId);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-07-13 23:09:18 +00:00
|
|
|
// Send the DOE
|
2022-07-01 00:52:03 +00:00
|
|
|
conversationIds.forEach(cid => {
|
2022-07-13 23:09:18 +00:00
|
|
|
// Don't DOE yourself!
|
|
|
|
if (cid === ourConversation.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-01 00:52:03 +00:00
|
|
|
const conversation = window.ConversationController.get(cid);
|
|
|
|
|
|
|
|
if (!conversation) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sendDeleteForEveryoneMessage(conversation.attributes, {
|
|
|
|
deleteForEveryoneDuration: DAY,
|
|
|
|
id: story.messageId,
|
|
|
|
timestamp: story.timestamp,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-07-13 23:09:18 +00:00
|
|
|
// If it's the last story sent to a distribution list we don't have to send
|
|
|
|
// the sync message, but to be consistent let's build up the updated
|
|
|
|
// storyMessageRecipients and send the sync message.
|
|
|
|
if (!updatedStoryRecipients.size) {
|
|
|
|
story.sendState.forEach(item => {
|
|
|
|
if (item.recipient.id === ourConversation.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const destinationUuid = window.ConversationController.get(
|
|
|
|
item.recipient.id
|
|
|
|
)?.get('uuid');
|
|
|
|
|
|
|
|
if (!destinationUuid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
updatedStoryRecipients.set(destinationUuid, {
|
|
|
|
distributionListIds: new Set(),
|
|
|
|
isAllowedToReply: item.isAllowedToReplyToStory !== false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send the sync message with the updated storyMessageRecipients list
|
|
|
|
const sender = window.textsecure.messaging;
|
|
|
|
if (sender) {
|
|
|
|
const options = await getSendOptions(ourConversation.attributes, {
|
|
|
|
syncMessage: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const storyMessageRecipients: Array<{
|
|
|
|
destinationUuid: string;
|
|
|
|
distributionListIds: Array<string>;
|
|
|
|
isAllowedToReply: boolean;
|
|
|
|
}> = [];
|
|
|
|
|
|
|
|
updatedStoryRecipients.forEach((recipientData, destinationUuid) => {
|
|
|
|
storyMessageRecipients.push({
|
|
|
|
destinationUuid,
|
|
|
|
distributionListIds: Array.from(recipientData.distributionListIds),
|
|
|
|
isAllowedToReply: recipientData.isAllowedToReply,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
const destinationUuid = ourConversation.get('uuid');
|
|
|
|
|
|
|
|
if (!destinationUuid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sync message for other devices
|
|
|
|
sender.sendSyncMessage({
|
|
|
|
destination: undefined,
|
|
|
|
destinationUuid,
|
|
|
|
storyMessageRecipients,
|
|
|
|
expirationStartTimestamp: null,
|
|
|
|
isUpdate: true,
|
|
|
|
options,
|
|
|
|
timestamp: story.timestamp,
|
|
|
|
urgent: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Sync message for Desktop
|
|
|
|
const ev = new StoryRecipientUpdateEvent(
|
|
|
|
{
|
|
|
|
destinationUuid,
|
|
|
|
timestamp: story.timestamp,
|
|
|
|
storyMessageRecipients,
|
|
|
|
},
|
|
|
|
noop
|
|
|
|
);
|
|
|
|
onStoryRecipientUpdate(ev);
|
|
|
|
}
|
|
|
|
|
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> {
|
2022-05-11 22:20:47 +00:00
|
|
|
return async (dispatch, getState) => {
|
|
|
|
const conversation = getConversationSelector(getState())(conversationId);
|
2022-04-15 00:08:46 +00:00
|
|
|
const replies = await dataInterface.getOlderMessagesByConversation(
|
|
|
|
conversationId,
|
2022-05-11 22:20:47 +00:00
|
|
|
{ limit: 9000, storyId: messageId, isGroup: 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)
|
|
|
|
) {
|
2022-04-08 15:40:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
if (matchingStory.readStatus !== ReadStatus.Unread) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const message = await getMessageById(messageId);
|
|
|
|
|
|
|
|
if (!message) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
const storyReadDate = Date.now();
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
const viewSyncs: Array<SyncType> = [viewedReceipt];
|
|
|
|
|
|
|
|
if (!window.ConversationController.areWePrimaryDevice()) {
|
|
|
|
viewSyncJobQueue.add({ viewSyncs });
|
|
|
|
}
|
|
|
|
|
|
|
|
viewedReceiptsJobQueue.add({ viewedReceipt });
|
|
|
|
|
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,
|
|
|
|
payload: messageId,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function queueStoryDownload(
|
|
|
|
storyId: string
|
2022-04-25 17:25:50 +00:00
|
|
|
): ThunkAction<
|
|
|
|
void,
|
|
|
|
RootStateType,
|
|
|
|
unknown,
|
|
|
|
NoopActionType | ResolveAttachmentUrlActionType
|
|
|
|
> {
|
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;
|
|
|
|
}
|
|
|
|
|
2022-07-01 00:52:03 +00:00
|
|
|
const { attachment } = 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)) {
|
2022-04-25 17:25:50 +00:00
|
|
|
if (!attachment.path) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This function also resolves the attachment's URL in case we've already
|
|
|
|
// downloaded the attachment but haven't pointed its path to an absolute
|
|
|
|
// location on disk.
|
|
|
|
if (hasNotResolved(attachment)) {
|
|
|
|
dispatch({
|
|
|
|
type: RESOLVE_ATTACHMENT_URL,
|
|
|
|
payload: {
|
|
|
|
messageId: storyId,
|
|
|
|
attachmentUrl: window.Signal.Migrations.getAbsoluteAttachmentPath(
|
|
|
|
attachment.path
|
|
|
|
),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isDownloading(attachment)) {
|
|
|
|
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
|
|
|
|
2022-07-01 00:52:03 +00:00
|
|
|
await queueAttachmentDownloads(message.attributes);
|
|
|
|
}
|
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,
|
2022-03-11 23:14:32 +00:00
|
|
|
messageBody: string,
|
2022-03-04 21:14:52 +00:00
|
|
|
mentions: Array<BodyRangeType>,
|
|
|
|
timestamp: number,
|
|
|
|
story: StoryViewType
|
2022-04-15 00:08:46 +00:00
|
|
|
): ThunkAction<void, RootStateType, unknown, ReplyToStoryActionType> {
|
|
|
|
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
|
|
|
|
2022-04-15 00:08:46 +00:00
|
|
|
const messageAttributes = await conversation.enqueueMessageForSend(
|
2022-04-11 20:57:44 +00:00
|
|
|
{
|
|
|
|
body: messageBody,
|
|
|
|
attachments: [],
|
|
|
|
mentions,
|
|
|
|
},
|
2022-03-04 21:14:52 +00:00
|
|
|
{
|
|
|
|
storyId: story.messageId,
|
|
|
|
timestamp,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-04-15 00:08:46 +00:00
|
|
|
if (messageAttributes) {
|
|
|
|
dispatch({
|
|
|
|
type: REPLY_TO_STORY,
|
|
|
|
payload: messageAttributes,
|
|
|
|
});
|
|
|
|
}
|
2022-03-04 21:14:52 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-08-02 19:31:55 +00:00
|
|
|
function sendStoryMessage(
|
|
|
|
listIds: Array<UUIDStringType>,
|
2022-08-04 19:23:24 +00:00
|
|
|
attachment: AttachmentType
|
2022-08-02 19:31:55 +00:00
|
|
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
|
|
|
return async dispatch => {
|
2022-08-04 19:23:24 +00:00
|
|
|
await doSendStoryMessage(listIds, attachment);
|
2022-08-02 19:31:55 +00:00
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: 'NOOP',
|
|
|
|
payload: null,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
function storyChanged(story: StoryDataType): StoryChangedActionType {
|
|
|
|
return {
|
|
|
|
type: STORY_CHANGED,
|
|
|
|
payload: story,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
function toggleStoriesView(): ToggleViewActionType {
|
|
|
|
return {
|
|
|
|
type: TOGGLE_VIEW,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-07-06 19:06:20 +00:00
|
|
|
const getSelectedStoryDataForConversationId = (
|
|
|
|
dispatch: ThunkDispatch<
|
|
|
|
RootStateType,
|
|
|
|
unknown,
|
|
|
|
NoopActionType | ResolveAttachmentUrlActionType
|
|
|
|
>,
|
|
|
|
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
|
|
|
|
// oldest unread story from the user. If all stories are read then we can
|
|
|
|
// start at the first story.
|
|
|
|
let currentIndex = 0;
|
|
|
|
let hasUnread = false;
|
|
|
|
storiesByConversationId.forEach((item, index) => {
|
|
|
|
if (selectedStoryId && item.messageId === selectedStoryId) {
|
|
|
|
currentIndex = index;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!selectedStoryId &&
|
|
|
|
!currentIndex &&
|
|
|
|
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(item => {
|
|
|
|
if (isDownloaded(item.attachment) || isDownloading(item.attachment)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
queueStoryDownload(item.messageId)(dispatch, getState, null);
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
currentIndex,
|
|
|
|
hasUnread,
|
|
|
|
numStories,
|
|
|
|
storiesByConversationId,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
function viewUserStories(
|
2022-07-25 18:55:44 +00:00
|
|
|
conversationId: string,
|
|
|
|
shouldShowDetailsModal = false
|
2022-07-06 19:06:20 +00:00
|
|
|
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> {
|
|
|
|
return (dispatch, getState) => {
|
|
|
|
const { currentIndex, hasUnread, numStories, storiesByConversationId } =
|
|
|
|
getSelectedStoryDataForConversationId(dispatch, getState, conversationId);
|
|
|
|
|
|
|
|
const story = storiesByConversationId[currentIndex];
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId: story.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode: hasUnread
|
|
|
|
? StoryViewModeType.Unread
|
|
|
|
: StoryViewModeType.All,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-07-25 18:55:44 +00:00
|
|
|
export type ViewStoryActionCreatorType = (opts: {
|
|
|
|
closeViewer?: boolean;
|
|
|
|
storyId?: string;
|
|
|
|
storyViewMode?: StoryViewModeType;
|
|
|
|
viewDirection?: StoryViewDirectionType;
|
|
|
|
shouldShowDetailsModal?: boolean;
|
|
|
|
}) => unknown;
|
|
|
|
|
|
|
|
const viewStory: ViewStoryActionCreatorType = ({
|
|
|
|
closeViewer,
|
|
|
|
shouldShowDetailsModal = false,
|
2022-07-06 19:06:20 +00:00
|
|
|
storyId,
|
|
|
|
storyViewMode,
|
2022-07-25 18:55:44 +00:00
|
|
|
viewDirection,
|
|
|
|
}): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
2022-07-06 19:06:20 +00:00
|
|
|
return (dispatch, getState) => {
|
2022-07-25 18:55:44 +00:00
|
|
|
if (closeViewer || !storyId || !storyViewMode) {
|
2022-07-06 19:06:20 +00:00
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: undefined,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const state = getState();
|
|
|
|
const { stories } = state.stories;
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { currentIndex, numStories, storiesByConversationId } =
|
|
|
|
getSelectedStoryDataForConversationId(
|
|
|
|
dispatch,
|
|
|
|
getState,
|
|
|
|
story.conversationId,
|
|
|
|
storyId
|
|
|
|
);
|
|
|
|
|
|
|
|
// Go directly to the storyId selected
|
|
|
|
if (!viewDirection) {
|
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId: storyId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
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: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex: nextIndex,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId: nextStory.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal: false,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
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: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex: nextIndex,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId: nextStory.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal: false,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are there any unviewed stories left? If so we should play the unviewed
|
|
|
|
// stories first. But only if we're going "next"
|
|
|
|
if (viewDirection === StoryViewDirectionType.Next) {
|
|
|
|
const unreadStory = stories.find(
|
2022-07-29 18:12:47 +00:00
|
|
|
item =>
|
|
|
|
item.readStatus === ReadStatus.Unread && !item.deletedForEveryone
|
2022-07-06 19:06:20 +00:00
|
|
|
);
|
|
|
|
if (unreadStory) {
|
|
|
|
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
|
|
|
|
dispatch,
|
|
|
|
getState,
|
|
|
|
unreadStory.conversationId,
|
|
|
|
unreadStory.messageId
|
|
|
|
);
|
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex: nextSelectedStoryData.currentIndex,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId: unreadStory.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories: nextSelectedStoryData.numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal: false,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const conversationStories = getStories(state).stories;
|
|
|
|
const conversationStoryIndex = conversationStories.findIndex(
|
|
|
|
item => item.conversationId === story.conversationId
|
|
|
|
);
|
|
|
|
|
|
|
|
if (conversationStoryIndex < 0) {
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
|
|
// Close the viewer if we were viewing unread stories only and we've
|
|
|
|
// reached the last unread story.
|
|
|
|
if (
|
|
|
|
!nextSelectedStoryData.hasUnread &&
|
|
|
|
storyViewMode === StoryViewModeType.Unread
|
|
|
|
) {
|
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: undefined,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex: 0,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId:
|
|
|
|
nextSelectedStoryData.storiesByConversationId[0].messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories: nextSelectedStoryData.numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal: false,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
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: {
|
|
|
|
selectedStoryData: {
|
|
|
|
currentIndex: 0,
|
2022-07-29 20:22:55 +00:00
|
|
|
messageId:
|
|
|
|
nextSelectedStoryData.storiesByConversationId[0].messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories: nextSelectedStoryData.numStories,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal: false,
|
2022-07-06 19:06:20 +00:00
|
|
|
},
|
|
|
|
storyViewMode,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Could not meet any criteria, close the viewer
|
|
|
|
dispatch({
|
|
|
|
type: VIEW_STORY,
|
|
|
|
payload: undefined,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const actions = {
|
|
|
|
deleteStoryForEveryone,
|
|
|
|
loadStoryReplies,
|
|
|
|
markStoryRead,
|
|
|
|
queueStoryDownload,
|
|
|
|
reactToStory,
|
|
|
|
replyToStory,
|
2022-08-02 19:31:55 +00:00
|
|
|
sendStoryMessage,
|
2022-07-06 19:06:20 +00:00
|
|
|
storyChanged,
|
|
|
|
toggleStoriesView,
|
|
|
|
viewUserStories,
|
|
|
|
viewStory,
|
|
|
|
};
|
|
|
|
|
|
|
|
export const useStoriesActions = (): typeof actions => useBoundActions(actions);
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
// Reducer
|
|
|
|
|
|
|
|
export function getEmptyState(
|
|
|
|
overrideState: Partial<StoriesStateType> = {}
|
|
|
|
): StoriesStateType {
|
|
|
|
return {
|
|
|
|
isShowingStoriesView: false,
|
|
|
|
stories: [],
|
|
|
|
...overrideState,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function reducer(
|
|
|
|
state: Readonly<StoriesStateType> = getEmptyState(),
|
|
|
|
action: Readonly<StoriesActionType>
|
|
|
|
): StoriesStateType {
|
|
|
|
if (action.type === TOGGLE_VIEW) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
isShowingStoriesView: !state.isShowingStoriesView,
|
2022-07-22 01:38:27 +00:00
|
|
|
selectedStoryData: state.isShowingStoriesView
|
|
|
|
? undefined
|
|
|
|
: state.selectedStoryData,
|
|
|
|
storyViewMode: state.isShowingStoriesView
|
|
|
|
? undefined
|
|
|
|
: state.storyViewMode,
|
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',
|
2022-03-04 21:14:52 +00:00
|
|
|
'messageId',
|
2022-04-28 22:06:28 +00:00
|
|
|
'reactions',
|
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-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 ||
|
|
|
|
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) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
stories: state.stories.map(story => {
|
|
|
|
if (story.messageId === action.payload) {
|
|
|
|
return {
|
|
|
|
...story,
|
|
|
|
readStatus: ReadStatus.Viewed,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return story;
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-15 00:08:46 +00:00
|
|
|
if (action.type === LOAD_STORY_REPLIES) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
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) {
|
|
|
|
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],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-25 17:25:50 +00:00
|
|
|
if (action.type === RESOLVE_ATTACHMENT_URL) {
|
|
|
|
const { messageId, attachmentUrl } = action.payload;
|
|
|
|
|
|
|
|
const storyIndex = state.stories.findIndex(
|
|
|
|
existingStory => existingStory.messageId === messageId
|
|
|
|
);
|
|
|
|
|
|
|
|
if (storyIndex < 0) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const story = state.stories[storyIndex];
|
|
|
|
|
|
|
|
if (!story.attachment) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const storyWithResolvedAttachment = {
|
|
|
|
...story,
|
|
|
|
attachment: {
|
|
|
|
...story.attachment,
|
|
|
|
url: attachmentUrl,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
stories: replaceIndex(
|
|
|
|
state.stories,
|
|
|
|
storyIndex,
|
|
|
|
storyWithResolvedAttachment
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
const { selectedStoryData, storyViewMode } = action.payload || {};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedStoryData,
|
|
|
|
storyViewMode,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
return state;
|
|
|
|
}
|