Fixes story play order
This commit is contained in:
parent
4308739bc0
commit
0be580e8e5
4 changed files with 471 additions and 30 deletions
|
@ -40,7 +40,10 @@ import {
|
|||
isDownloaded,
|
||||
isDownloading,
|
||||
} from '../../types/Attachment';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getHideStoryConversationIds,
|
||||
} from '../selectors/conversations';
|
||||
import { getSendOptions } from '../../util/getSendOptions';
|
||||
import { getStories } from '../selectors/stories';
|
||||
import { getStoryDataFromMessageAttributes } from '../../services/storyLoader';
|
||||
|
@ -826,19 +829,25 @@ const viewUserStories: ViewUserStoriesActionCreatorType = ({
|
|||
};
|
||||
};
|
||||
|
||||
type ViewStoryOptionsType =
|
||||
| {
|
||||
closeViewer: true;
|
||||
}
|
||||
| {
|
||||
storyId: string;
|
||||
storyViewMode: StoryViewModeType;
|
||||
viewDirection?: StoryViewDirectionType;
|
||||
shouldShowDetailsModal?: boolean;
|
||||
};
|
||||
|
||||
export type ViewStoryActionCreatorType = (
|
||||
opts:
|
||||
| {
|
||||
closeViewer: true;
|
||||
}
|
||||
| {
|
||||
storyId: string;
|
||||
storyViewMode: StoryViewModeType;
|
||||
viewDirection?: StoryViewDirectionType;
|
||||
shouldShowDetailsModal?: boolean;
|
||||
}
|
||||
opts: ViewStoryOptionsType
|
||||
) => unknown;
|
||||
|
||||
export type DispatchableViewStoryType = (
|
||||
opts: ViewStoryOptionsType
|
||||
) => ThunkAction<void, RootStateType, unknown, ViewStoryActionType>;
|
||||
|
||||
const viewStory: ViewStoryActionCreatorType = (
|
||||
opts
|
||||
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
||||
|
@ -954,10 +963,20 @@ const viewStory: ViewStoryActionCreatorType = (
|
|||
// 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(
|
||||
item =>
|
||||
item.readStatus === ReadStatus.Unread && !item.deletedForEveryone
|
||||
// Only stories that succeed the current story we're on.
|
||||
const currentStoryIndex = stories.findIndex(
|
||||
item => item.messageId === storyId
|
||||
);
|
||||
// No hidden stories
|
||||
const hiddenConversationIds = new Set(getHideStoryConversationIds(state));
|
||||
const unreadStory = stories.find(
|
||||
(item, index) =>
|
||||
index > currentStoryIndex &&
|
||||
!item.deletedForEveryone &&
|
||||
item.readStatus === ReadStatus.Unread &&
|
||||
!hiddenConversationIds.has(item.conversationId)
|
||||
);
|
||||
|
||||
if (unreadStory) {
|
||||
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
|
||||
dispatch,
|
||||
|
@ -977,6 +996,16 @@ const viewStory: ViewStoryActionCreatorType = (
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the viewer if we were viewing unread stories only and we did not
|
||||
// find any more unread.
|
||||
if (storyViewMode === StoryViewModeType.Unread) {
|
||||
dispatch({
|
||||
type: VIEW_STORY,
|
||||
payload: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const conversationStories = getStories(state).stories;
|
||||
|
@ -1018,19 +1047,6 @@ const viewStory: ViewStoryActionCreatorType = (
|
|||
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: {
|
||||
|
|
|
@ -1050,3 +1050,11 @@ export const getConversationsStoppingSend = createSelector(
|
|||
return sortByTitle(conversations);
|
||||
}
|
||||
);
|
||||
|
||||
export const getHideStoryConversationIds = createSelector(
|
||||
getConversationLookup,
|
||||
(conversationLookup): Array<string> =>
|
||||
Object.keys(conversationLookup).filter(
|
||||
conversationId => conversationLookup[conversationId].hideStory
|
||||
)
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ import { canReply } from './message';
|
|||
import {
|
||||
getContactNameColorSelector,
|
||||
getConversationSelector,
|
||||
getHideStoryConversationIds,
|
||||
getMe,
|
||||
} from './conversations';
|
||||
import { getUserConversationId } from './user';
|
||||
|
@ -412,15 +413,19 @@ export const getStories = createSelector(
|
|||
);
|
||||
|
||||
export const getStoriesNotificationCount = createSelector(
|
||||
getHideStoryConversationIds,
|
||||
getStoriesState,
|
||||
({ lastOpenedAtTimestamp, stories }): number => {
|
||||
(hideStoryConversationIds, { lastOpenedAtTimestamp, stories }): number => {
|
||||
const hiddenConversationIds = new Set(hideStoryConversationIds);
|
||||
|
||||
return new Set(
|
||||
stories
|
||||
.filter(
|
||||
story =>
|
||||
story.readStatus === ReadStatus.Unread &&
|
||||
!story.deletedForEveryone &&
|
||||
story.timestamp > (lastOpenedAtTimestamp || 0)
|
||||
story.timestamp > (lastOpenedAtTimestamp || 0) &&
|
||||
!hiddenConversationIds.has(story.conversationId)
|
||||
)
|
||||
.map(story => story.conversationId)
|
||||
).size;
|
||||
|
|
|
@ -2,13 +2,26 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import casual from 'casual';
|
||||
import path from 'path';
|
||||
import { assert } from 'chai';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { StoriesStateType } from '../../../state/ducks/stories';
|
||||
import type {
|
||||
DispatchableViewStoryType,
|
||||
StoriesStateType,
|
||||
StoryDataType,
|
||||
} from '../../../state/ducks/stories';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import type { MessageAttributesType } from '../../../model-types.d';
|
||||
import type { StateType as RootStateType } from '../../../state/reducer';
|
||||
import { DAY } from '../../../util/durations';
|
||||
import { IMAGE_JPEG } from '../../../types/MIME';
|
||||
import { ReadStatus } from '../../../messages/MessageReadStatus';
|
||||
import {
|
||||
StoryViewDirectionType,
|
||||
StoryViewModeType,
|
||||
} from '../../../types/Stories';
|
||||
import {
|
||||
actions,
|
||||
getEmptyState,
|
||||
|
@ -38,6 +51,405 @@ describe('both/state/ducks/stories', () => {
|
|||
};
|
||||
}
|
||||
|
||||
describe('viewStory', () => {
|
||||
function getMockConversation({
|
||||
id: conversationId,
|
||||
hideStory = false,
|
||||
}: Pick<ConversationType, 'id' | 'hideStory'>): ConversationType {
|
||||
return {
|
||||
acceptedMessageRequest: true,
|
||||
badges: [],
|
||||
hideStory,
|
||||
id: conversationId,
|
||||
isMe: false,
|
||||
sharedGroupNames: [],
|
||||
title: casual.username,
|
||||
type: 'direct' as const,
|
||||
};
|
||||
}
|
||||
|
||||
function getStoryData(
|
||||
messageId: string,
|
||||
conversationId = uuid()
|
||||
): StoryDataType {
|
||||
const now = Date.now();
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
expirationStartTimestamp: now,
|
||||
expireTimer: 1 * DAY,
|
||||
messageId,
|
||||
readStatus: ReadStatus.Unread,
|
||||
timestamp: now,
|
||||
type: 'story',
|
||||
};
|
||||
}
|
||||
|
||||
function getStateFunction(
|
||||
stories: Array<StoryDataType>,
|
||||
conversationLookup: { [key: string]: ConversationType } = {}
|
||||
): () => RootStateType {
|
||||
const rootState = getEmptyRootState();
|
||||
|
||||
return () => ({
|
||||
...rootState,
|
||||
conversations: {
|
||||
...rootState.conversations,
|
||||
conversationLookup,
|
||||
},
|
||||
stories: {
|
||||
...rootState.stories,
|
||||
stories,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const viewStory = actions.viewStory as DispatchableViewStoryType;
|
||||
|
||||
it('closes the viewer', () => {
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
viewStory({ closeViewer: true })(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not find a story', () => {
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: uuid(),
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
})(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('selects a specific story', () => {
|
||||
const storyId = uuid();
|
||||
|
||||
const getState = getStateFunction([getStoryData(storyId)]);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId,
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: {
|
||||
currentIndex: 0,
|
||||
messageId: storyId,
|
||||
numStories: 1,
|
||||
shouldShowDetailsModal: false,
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("navigating within a user's stories", () => {
|
||||
it('selects the next story', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const conversationId = uuid();
|
||||
const getState = getStateFunction([
|
||||
getStoryData(storyId1, conversationId),
|
||||
getStoryData(storyId2, conversationId),
|
||||
getStoryData(storyId3, conversationId),
|
||||
]);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId1,
|
||||
storyViewMode: StoryViewModeType.User,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: {
|
||||
currentIndex: 1,
|
||||
messageId: storyId2,
|
||||
numStories: 3,
|
||||
shouldShowDetailsModal: false,
|
||||
storyViewMode: StoryViewModeType.User,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('selects the prev story', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const conversationId = uuid();
|
||||
const getState = getStateFunction([
|
||||
getStoryData(storyId1, conversationId),
|
||||
getStoryData(storyId2, conversationId),
|
||||
getStoryData(storyId3, conversationId),
|
||||
]);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId2,
|
||||
storyViewMode: StoryViewModeType.User,
|
||||
viewDirection: StoryViewDirectionType.Previous,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: {
|
||||
currentIndex: 0,
|
||||
messageId: storyId1,
|
||||
numStories: 3,
|
||||
shouldShowDetailsModal: false,
|
||||
storyViewMode: StoryViewModeType.User,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('when in StoryViewModeType.User and we have reached the end, it closes the viewer', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const conversationId = uuid();
|
||||
const getState = getStateFunction([
|
||||
getStoryData(storyId1, conversationId),
|
||||
getStoryData(storyId2, conversationId),
|
||||
getStoryData(storyId3, conversationId),
|
||||
]);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId3,
|
||||
storyViewMode: StoryViewModeType.User,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unviewed stories', () => {
|
||||
it('finds any unviewed stories and selects them', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const getState = getStateFunction([
|
||||
getStoryData(storyId1),
|
||||
{
|
||||
...getStoryData(storyId2),
|
||||
readStatus: ReadStatus.Viewed,
|
||||
},
|
||||
getStoryData(storyId3),
|
||||
]);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId1,
|
||||
storyViewMode: StoryViewModeType.Unread,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: {
|
||||
currentIndex: 0,
|
||||
messageId: storyId3,
|
||||
numStories: 1,
|
||||
shouldShowDetailsModal: false,
|
||||
storyViewMode: StoryViewModeType.Unread,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not select hidden stories', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const conversationId = uuid();
|
||||
|
||||
const getState = getStateFunction(
|
||||
[
|
||||
getStoryData(storyId1),
|
||||
getStoryData(storyId2, conversationId),
|
||||
getStoryData(storyId3, conversationId),
|
||||
],
|
||||
{
|
||||
[conversationId]: getMockConversation({
|
||||
id: conversationId,
|
||||
hideStory: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId1,
|
||||
storyViewMode: StoryViewModeType.Unread,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not select stories that precede the currently viewed story', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const getState = getStateFunction([
|
||||
getStoryData(storyId1),
|
||||
getStoryData(storyId2),
|
||||
getStoryData(storyId3),
|
||||
]);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId3,
|
||||
storyViewMode: StoryViewModeType.Unread,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('closes the viewer when there are no more unviewed stories', () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
|
||||
const conversationId1 = uuid();
|
||||
const conversationId2 = uuid();
|
||||
|
||||
const getState = getStateFunction(
|
||||
[
|
||||
getStoryData(storyId1, conversationId1),
|
||||
{
|
||||
...getStoryData(storyId2, conversationId2),
|
||||
readStatus: ReadStatus.Viewed,
|
||||
},
|
||||
],
|
||||
{
|
||||
[conversationId1]: getMockConversation({ id: conversationId1 }),
|
||||
[conversationId2]: getMockConversation({ id: conversationId2 }),
|
||||
}
|
||||
);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId1,
|
||||
storyViewMode: StoryViewModeType.Unread,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('paging through collections of stories', () => {
|
||||
function getViewedStoryData(
|
||||
storyId: string,
|
||||
conversationId?: string
|
||||
): StoryDataType {
|
||||
return {
|
||||
...getStoryData(storyId, conversationId),
|
||||
readStatus: ReadStatus.Viewed,
|
||||
};
|
||||
}
|
||||
|
||||
it("goes to the next user's stories", () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const conversationId2 = uuid();
|
||||
const conversationId1 = uuid();
|
||||
const getState = getStateFunction(
|
||||
[
|
||||
getViewedStoryData(storyId1, conversationId1),
|
||||
getViewedStoryData(storyId2, conversationId2),
|
||||
getViewedStoryData(storyId3, conversationId2),
|
||||
],
|
||||
{
|
||||
[conversationId1]: getMockConversation({ id: conversationId1 }),
|
||||
[conversationId2]: getMockConversation({ id: conversationId2 }),
|
||||
}
|
||||
);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId1,
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
viewDirection: StoryViewDirectionType.Next,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: {
|
||||
currentIndex: 0,
|
||||
messageId: storyId2,
|
||||
numStories: 2,
|
||||
shouldShowDetailsModal: false,
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("goes to the prev user's stories", () => {
|
||||
const storyId1 = uuid();
|
||||
const storyId2 = uuid();
|
||||
const storyId3 = uuid();
|
||||
const conversationId1 = uuid();
|
||||
const conversationId2 = uuid();
|
||||
const getState = getStateFunction(
|
||||
[
|
||||
getViewedStoryData(storyId1, conversationId2),
|
||||
getViewedStoryData(storyId2, conversationId1),
|
||||
getViewedStoryData(storyId3, conversationId2),
|
||||
],
|
||||
{
|
||||
[conversationId1]: getMockConversation({ id: conversationId1 }),
|
||||
[conversationId2]: getMockConversation({ id: conversationId2 }),
|
||||
}
|
||||
);
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
viewStory({
|
||||
storyId: storyId2,
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
viewDirection: StoryViewDirectionType.Previous,
|
||||
})(dispatch, getState, null);
|
||||
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'stories/VIEW_STORY',
|
||||
payload: {
|
||||
currentIndex: 0,
|
||||
messageId: storyId1,
|
||||
numStories: 2,
|
||||
shouldShowDetailsModal: false,
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('queueStoryDownload', () => {
|
||||
const { queueStoryDownload } = actions;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue