Story viewer navigation improvements

This commit is contained in:
Josh Perez 2022-10-17 12:33:07 -04:00 committed by GitHub
parent 0e49f7906d
commit 4882248041
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 670 additions and 144 deletions

View file

@ -88,7 +88,7 @@ export const MyStories = ({
onClick={() =>
viewStory({
storyId: story.messageId,
storyViewMode: StoryViewModeType.User,
storyViewMode: StoryViewModeType.MyStories,
})
}
type="button"
@ -151,7 +151,7 @@ export const MyStories = ({
onClick: () => {
viewStory({
storyId: story.messageId,
storyViewMode: StoryViewModeType.User,
storyViewMode: StoryViewModeType.MyStories,
viewTarget: StoryViewTargetType.Details,
});
},

View file

@ -319,6 +319,7 @@ export const StoryViewer = ({
const canFreelyNavigateStories =
storyViewMode === StoryViewModeType.All ||
storyViewMode === StoryViewModeType.Hidden ||
storyViewMode === StoryViewModeType.MyStories ||
storyViewMode === StoryViewModeType.Unread;
const canNavigateLeft =

View file

@ -82,8 +82,9 @@ export type SelectedStoryDataType = {
currentIndex: number;
messageId: string;
numStories: number;
viewTarget?: StoryViewTargetType;
storyViewMode: StoryViewModeType;
unviewedStoryConversationIdsSorted: Array<string>;
viewTarget?: StoryViewTargetType;
};
// State
@ -592,6 +593,38 @@ function verifyStoryListMembers(
};
}
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: [],
};
};
const getSelectedStoryDataForConversationId = (
dispatch: ThunkDispatch<
RootStateType,
@ -615,7 +648,7 @@ const getSelectedStoryDataForConversationId = (
);
// 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
// oldest unviewed story from the user. If all stories are read then we can
// start at the first story.
let currentIndex: number | undefined;
let hasUnread = false;
@ -655,24 +688,23 @@ const getSelectedStoryDataForConversationId = (
export type ViewUserStoriesActionCreatorType = (opts: {
conversationId: string;
viewTarget?: StoryViewTargetType;
storyViewMode?: StoryViewModeType;
viewTarget?: StoryViewTargetType;
}) => unknown;
const viewUserStories: ViewUserStoriesActionCreatorType = ({
conversationId,
viewTarget,
storyViewMode,
viewTarget,
}): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
return (dispatch, getState) => {
const { currentIndex, hasUnread, numStories, storiesByConversationId } =
getSelectedStoryDataForConversationId(dispatch, getState, conversationId);
const story = storiesByConversationId[currentIndex];
const state = getState();
const hiddenConversationIds = new Set(
getHideStoryConversationIds(getState())
);
const hiddenConversationIds = new Set(getHideStoryConversationIds(state));
let inferredStoryViewMode: StoryViewModeType;
if (storyViewMode) {
@ -685,14 +717,30 @@ const viewUserStories: ViewUserStoriesActionCreatorType = ({
inferredStoryViewMode = StoryViewModeType.All;
}
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);
}
dispatch({
type: VIEW_STORY,
payload: {
currentIndex,
messageId: story.messageId,
numStories,
viewTarget,
storyViewMode: inferredStoryViewMode,
unviewedStoryConversationIdsSorted,
viewTarget,
},
});
};
@ -732,7 +780,10 @@ const viewStory: ViewStoryActionCreatorType = (
const { viewTarget, storyId, storyViewMode, viewDirection } = opts;
const state = getState();
const { stories } = state.stories;
const { selectedStoryData, stories } = state.stories;
const unviewedStoryConversationIdsSorted =
selectedStoryData?.unviewedStoryConversationIdsSorted || [];
// Spec:
// When opening the story viewer you should always be taken to the oldest
@ -754,12 +805,18 @@ const viewStory: ViewStoryActionCreatorType = (
}
const { currentIndex, numStories, storiesByConversationId } =
getSelectedStoryDataForConversationId(
dispatch,
getState,
story.conversationId,
storyId
);
storyViewMode === StoryViewModeType.MyStories
? getSelectedStoryDataForDistributionListId(
getState,
story.storyDistributionListId,
storyId
)
: getSelectedStoryDataForConversationId(
dispatch,
getState,
story.conversationId,
storyId
);
// Go directly to the storyId selected
if (!viewDirection) {
@ -769,8 +826,101 @@ const viewStory: ViewStoryActionCreatorType = (
currentIndex,
messageId: storyId,
numStories,
viewTarget,
storyViewMode,
unviewedStoryConversationIdsSorted,
viewTarget,
},
});
return;
}
// 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,
storyViewMode,
unviewedStoryConversationIdsSorted,
},
});
return;
@ -791,6 +941,7 @@ const viewStory: ViewStoryActionCreatorType = (
messageId: nextStory.messageId,
numStories,
storyViewMode,
unviewedStoryConversationIdsSorted,
},
});
return;
@ -808,6 +959,7 @@ const viewStory: ViewStoryActionCreatorType = (
messageId: nextStory.messageId,
numStories,
storyViewMode,
unviewedStoryConversationIdsSorted,
},
});
return;
@ -832,18 +984,27 @@ 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) {
// TODO: DESKTOP-4341 only stories that succeed the current story we're on.
const unreadStory = conversationStories.find(
item => item.storyView.isUnread
);
// stories first.
if (storyViewMode === StoryViewModeType.Unread) {
const frozenConversationStoryIndex =
unviewedStoryConversationIdsSorted.findIndex(
conversationId => conversationId === story.conversationId
);
if (unreadStory) {
let nextUnreadConversationId: string | undefined;
if (viewDirection === StoryViewDirectionType.Previous) {
nextUnreadConversationId =
unviewedStoryConversationIdsSorted[frozenConversationStoryIndex - 1];
} else if (viewDirection === StoryViewDirectionType.Next) {
nextUnreadConversationId =
unviewedStoryConversationIdsSorted[frozenConversationStoryIndex + 1];
}
if (nextUnreadConversationId) {
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
dispatch,
getState,
unreadStory.conversationId
nextUnreadConversationId
);
dispatch({
@ -856,20 +1017,19 @@ const viewStory: ViewStoryActionCreatorType = (
].messageId,
numStories: nextSelectedStoryData.numStories,
storyViewMode,
unviewedStoryConversationIdsSorted,
},
});
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;
}
// 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;
}
if (conversationStoryIndex < 0) {
@ -913,6 +1073,7 @@ const viewStory: ViewStoryActionCreatorType = (
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
numStories: nextSelectedStoryData.numStories,
storyViewMode,
unviewedStoryConversationIdsSorted,
},
});
return;
@ -947,6 +1108,7 @@ const viewStory: ViewStoryActionCreatorType = (
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
numStories: nextSelectedStoryData.numStories,
storyViewMode,
unviewedStoryConversationIdsSorted,
},
});
return;

View file

@ -5,7 +5,6 @@ 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 {
DispatchableViewStoryType,
@ -15,6 +14,7 @@ import type {
import type { ConversationType } from '../../../state/ducks/conversations';
import type { MessageAttributesType } from '../../../model-types.d';
import type { StateType as RootStateType } from '../../../state/reducer';
import type { UUIDStringType } from '../../../types/UUID';
import { DAY } from '../../../util/durations';
import { IMAGE_JPEG } from '../../../types/MIME';
import { ReadStatus } from '../../../messages/MessageReadStatus';
@ -22,6 +22,7 @@ import {
StoryViewDirectionType,
StoryViewModeType,
} from '../../../types/Stories';
import { UUID } from '../../../types/UUID';
import {
actions,
getEmptyState,
@ -31,7 +32,6 @@ import {
import { noopAction } from '../../../state/ducks/noop';
import { reducer as rootReducer } from '../../../state/reducer';
import { dropNull } from '../../../util/dropNull';
import type { UUIDStringType } from '../../../types/UUID';
describe('both/state/ducks/stories', () => {
const getEmptyRootState = () => ({
@ -43,7 +43,7 @@ describe('both/state/ducks/stories', () => {
const now = Date.now();
return {
conversationId: uuid(),
conversationId: UUID.generate().toString(),
id,
received_at: now,
sent_at: now,
@ -56,7 +56,10 @@ describe('both/state/ducks/stories', () => {
function getMockConversation({
id: conversationId,
hideStory = false,
}: Pick<ConversationType, 'id' | 'hideStory'>): ConversationType {
title,
}: Pick<ConversationType, 'id' | 'hideStory'> & {
title?: string;
}): ConversationType {
return {
acceptedMessageRequest: true,
badges: [],
@ -64,14 +67,15 @@ describe('both/state/ducks/stories', () => {
id: conversationId,
isMe: false,
sharedGroupNames: [],
title: casual.username,
title: title || casual.username,
type: 'direct' as const,
};
}
function getStoryData(
messageId: string,
conversationId = uuid()
conversationId = UUID.generate().toString(),
timestampDelta = 0
): StoryDataType {
const now = Date.now();
@ -81,14 +85,15 @@ describe('both/state/ducks/stories', () => {
expireTimer: 1 * DAY,
messageId,
readStatus: ReadStatus.Unread,
timestamp: now,
timestamp: now - timestampDelta,
type: 'story',
};
}
function getStateFunction(
stories: Array<StoryDataType>,
conversationLookup: { [key: string]: ConversationType } = {}
conversationLookup: { [key: string]: ConversationType } = {},
unviewedStoryConversationIdsSorted: Array<string> = []
): () => RootStateType {
const rootState = getEmptyRootState();
@ -100,6 +105,13 @@ describe('both/state/ducks/stories', () => {
},
stories: {
...rootState.stories,
selectedStoryData: {
currentIndex: 0,
messageId: '',
numStories: 0,
storyViewMode: StoryViewModeType.Unread,
unviewedStoryConversationIdsSorted,
},
stories,
},
});
@ -121,7 +133,7 @@ describe('both/state/ducks/stories', () => {
it('does not find a story', () => {
const dispatch = sinon.spy();
viewStory({
storyId: uuid(),
storyId: UUID.generate().toString(),
storyViewMode: StoryViewModeType.All,
})(dispatch, getEmptyRootState, null);
@ -132,7 +144,7 @@ describe('both/state/ducks/stories', () => {
});
it('selects a specific story', () => {
const storyId = uuid();
const storyId = UUID.generate().toString();
const getState = getStateFunction([getStoryData(storyId)]);
@ -148,18 +160,19 @@ describe('both/state/ducks/stories', () => {
currentIndex: 0,
messageId: storyId,
numStories: 1,
viewTarget: undefined,
storyViewMode: StoryViewModeType.All,
unviewedStoryConversationIdsSorted: [],
viewTarget: undefined,
},
});
});
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 storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const conversationId = UUID.generate().toString();
const getState = getStateFunction([
getStoryData(storyId1, conversationId),
getStoryData(storyId2, conversationId),
@ -180,15 +193,16 @@ describe('both/state/ducks/stories', () => {
messageId: storyId2,
numStories: 3,
storyViewMode: StoryViewModeType.User,
unviewedStoryConversationIdsSorted: [],
},
});
});
it('selects the prev story', () => {
const storyId1 = uuid();
const storyId2 = uuid();
const storyId3 = uuid();
const conversationId = uuid();
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const conversationId = UUID.generate().toString();
const getState = getStateFunction([
getStoryData(storyId1, conversationId),
getStoryData(storyId2, conversationId),
@ -209,15 +223,16 @@ describe('both/state/ducks/stories', () => {
messageId: storyId1,
numStories: 3,
storyViewMode: StoryViewModeType.User,
unviewedStoryConversationIdsSorted: [],
},
});
});
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 storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const conversationId = UUID.generate().toString();
const getState = getStateFunction([
getStoryData(storyId1, conversationId),
getStoryData(storyId2, conversationId),
@ -239,58 +254,12 @@ describe('both/state/ducks/stories', () => {
});
describe('unviewed stories', () => {
it('finds any unviewed stories and selects them', () => {
const storyId1 = uuid();
const storyId2 = uuid();
const storyId3 = uuid();
const convoId1 = uuid();
const convoId2 = uuid();
const convoId3 = uuid();
const getState = getStateFunction(
[
{
...getStoryData(storyId1, convoId1),
readStatus: ReadStatus.Viewed,
},
{
...getStoryData(storyId2, convoId2),
readStatus: ReadStatus.Viewed,
},
getStoryData(storyId3, convoId3),
],
{
[convoId1]: getMockConversation({ id: convoId1 }),
[convoId2]: getMockConversation({ id: convoId2 }),
[convoId3]: getMockConversation({ id: convoId3 }),
}
);
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,
storyViewMode: StoryViewModeType.Unread,
},
});
});
it('does not select hidden stories', () => {
const storyId1 = uuid();
const storyId2 = uuid();
const storyId3 = uuid();
const conversationId = uuid();
const conversationIdHide: UUIDStringType = 'test-convo-uuid-hide-story';
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const conversationId = UUID.generate().toString();
const conversationIdHide = UUID.generate().toString();
const getState = getStateFunction(
[
@ -315,7 +284,8 @@ describe('both/state/ducks/stories', () => {
id: conversationIdHide,
hideStory: true,
}),
}
},
[conversationId]
);
const dispatch = sinon.spy();
@ -331,21 +301,39 @@ describe('both/state/ducks/stories', () => {
});
});
// TODO: DESKTOP-4341 - removed until implemented
/*
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 storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const storyId4 = UUID.generate().toString();
const conversationId1 = UUID.generate().toString();
const conversationId2 = UUID.generate().toString();
const conversationId3 = UUID.generate().toString();
// conversationId3 - storyId4
// conversationId1 - storyId1, storyId3
// conversationId2 - storyId2
const getState = getStateFunction(
[
getStoryData(storyId1, conversationId1, 3),
{
...getStoryData(storyId2, conversationId2, 2),
readStatus: ReadStatus.Viewed,
},
getStoryData(storyId3, conversationId1, 1),
getStoryData(storyId4, conversationId3),
],
{
[conversationId1]: getMockConversation({ id: conversationId1 }),
[conversationId2]: getMockConversation({ id: conversationId2 }),
[conversationId3]: getMockConversation({ id: conversationId3 }),
},
[conversationId3, conversationId1, conversationId2]
);
const dispatch = sinon.spy();
viewStory({
storyId: storyId3,
storyId: storyId2,
storyViewMode: StoryViewModeType.Unread,
viewDirection: StoryViewDirectionType.Next,
})(dispatch, getState, null);
@ -355,14 +343,115 @@ describe('both/state/ducks/stories', () => {
payload: undefined,
});
});
*/
it('correctly goes to previous unviewed story', () => {
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const storyId4 = UUID.generate().toString();
const conversationId1 = UUID.generate().toString();
const conversationId2 = UUID.generate().toString();
const conversationId3 = UUID.generate().toString();
const unviewedStoryConversationIdsSorted = [
conversationId3,
conversationId1,
conversationId2,
];
const getState = getStateFunction(
[
getStoryData(storyId1, conversationId1, 3),
{
...getStoryData(storyId2, conversationId2, 2),
readStatus: ReadStatus.Viewed,
},
getStoryData(storyId3, conversationId1, 1),
getStoryData(storyId4, conversationId3),
],
{
[conversationId1]: getMockConversation({ id: conversationId1 }),
[conversationId2]: getMockConversation({ id: conversationId2 }),
[conversationId3]: getMockConversation({ id: conversationId3 }),
},
unviewedStoryConversationIdsSorted
);
const dispatch = sinon.spy();
viewStory({
storyId: storyId2,
storyViewMode: StoryViewModeType.Unread,
viewDirection: StoryViewDirectionType.Previous,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 0,
messageId: storyId1,
numStories: 2,
storyViewMode: StoryViewModeType.Unread,
unviewedStoryConversationIdsSorted,
},
});
});
it('does not close the viewer when playing the next story', () => {
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const storyId4 = UUID.generate().toString();
const conversationId1 = UUID.generate().toString();
const conversationId2 = UUID.generate().toString();
const conversationId3 = UUID.generate().toString();
const unviewedStoryConversationIdsSorted = [
conversationId3,
conversationId2,
conversationId1,
];
const getState = getStateFunction(
[
getStoryData(storyId1, conversationId2, 3),
getStoryData(storyId2, conversationId1, 2),
getStoryData(storyId3, conversationId2, 1),
{
...getStoryData(storyId4, conversationId3),
readStatus: ReadStatus.Viewed,
},
],
{
[conversationId1]: getMockConversation({ id: conversationId1 }),
[conversationId2]: getMockConversation({ id: conversationId2 }),
[conversationId3]: getMockConversation({ id: conversationId3 }),
},
unviewedStoryConversationIdsSorted
);
const dispatch = sinon.spy();
viewStory({
storyId: storyId4,
storyViewMode: StoryViewModeType.Unread,
viewDirection: StoryViewDirectionType.Next,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 0,
messageId: storyId1,
numStories: 2,
storyViewMode: StoryViewModeType.Unread,
unviewedStoryConversationIdsSorted,
},
});
});
it('closes the viewer when there are no more unviewed stories', () => {
const storyId1 = uuid();
const storyId2 = uuid();
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const conversationId1 = uuid();
const conversationId2 = uuid();
const conversationId1 = UUID.generate().toString();
const conversationId2 = UUID.generate().toString();
const getState = getStateFunction(
[
@ -378,7 +467,8 @@ describe('both/state/ducks/stories', () => {
{
[conversationId1]: getMockConversation({ id: conversationId1 }),
[conversationId2]: getMockConversation({ id: conversationId2 }),
}
},
[conversationId1]
);
const dispatch = sinon.spy();
@ -395,10 +485,280 @@ describe('both/state/ducks/stories', () => {
});
});
describe('paging through sent stories', () => {
function getSentStoryReduxData() {
const distributionListId1 = UUID.generate().toString();
const distributionListId2 = UUID.generate().toString();
const storyDistributionLists = {
distributionLists: [
{
id: distributionListId1,
name: 'List 1',
allowsReplies: true,
isBlockList: false,
memberUuids: [
UUID.generate().toString(),
UUID.generate().toString(),
UUID.generate().toString(),
],
},
{
id: distributionListId2,
name: 'List 2',
allowsReplies: true,
isBlockList: false,
memberUuids: [
UUID.generate().toString(),
UUID.generate().toString(),
UUID.generate().toString(),
],
},
],
};
const ourConversationId = UUID.generate().toString();
const groupConversationId = UUID.generate().toString();
function getMyStoryData(
messageId: string,
storyDistributionListId?: string,
timestampDelta = 0
): StoryDataType {
const now = Date.now();
return {
conversationId: storyDistributionListId
? ourConversationId
: groupConversationId,
expirationStartTimestamp: now,
expireTimer: 1 * DAY,
messageId,
readStatus: ReadStatus.Unread,
sendStateByConversationId: {},
storyDistributionListId,
timestamp: now - timestampDelta,
type: 'story',
};
}
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const storyId4 = UUID.generate().toString();
const storyId5 = UUID.generate().toString();
const myStories = [
getMyStoryData(storyId1, distributionListId1, 5),
getMyStoryData(storyId2, distributionListId2, 4),
getMyStoryData(storyId3, distributionListId1, 3),
getMyStoryData(storyId4, undefined, 2), // group story
getMyStoryData(storyId5, distributionListId2, 1),
];
const rootState = getEmptyRootState();
return {
storyId1,
storyId2,
storyId3,
storyId4,
storyId5,
getState: () => ({
...rootState,
conversations: {
...rootState.conversations,
conversationLookup: {
[groupConversationId]: getMockConversation({
id: groupConversationId,
title: 'Group',
}),
},
},
storyDistributionLists,
stories: {
...rootState.stories,
stories: myStories,
},
}),
};
}
it('closes the viewer when hitting next at the last item', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId3 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId3,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Next,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: undefined,
});
});
it('closes the viewer when hitting prev at the first item', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId2 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId2,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Previous,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: undefined,
});
});
it('goes to next story within a distribution list', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId1, storyId3 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId1,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Next,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 1,
messageId: storyId3,
numStories: 2,
storyViewMode: StoryViewModeType.MyStories,
unviewedStoryConversationIdsSorted: [],
},
});
});
it('goes to prev story within a distribution list', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId1, storyId3 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId3,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Previous,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 0,
messageId: storyId1,
numStories: 2,
storyViewMode: StoryViewModeType.MyStories,
unviewedStoryConversationIdsSorted: [],
},
});
});
it('goes to the next distribution list', () => {
const { getState, storyId4, storyId1 } = getSentStoryReduxData();
const dispatch = sinon.spy();
viewStory({
storyId: storyId4,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Next,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 0,
messageId: storyId1,
numStories: 2,
storyViewMode: StoryViewModeType.MyStories,
unviewedStoryConversationIdsSorted: [],
},
});
});
it('goes to the prev distribution list', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId4, storyId5 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId4,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Previous,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 1,
messageId: storyId5,
numStories: 2,
storyViewMode: StoryViewModeType.MyStories,
unviewedStoryConversationIdsSorted: [],
},
});
});
it('goes next to a group story', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId4, storyId5 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId5,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Next,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 0,
messageId: storyId4,
numStories: 1,
storyViewMode: StoryViewModeType.MyStories,
unviewedStoryConversationIdsSorted: [],
},
});
});
it('goes prev to a group story', () => {
const { getState, ...reduxData } = getSentStoryReduxData();
const { storyId1, storyId4 } = reduxData;
const dispatch = sinon.spy();
viewStory({
storyId: storyId1,
storyViewMode: StoryViewModeType.MyStories,
viewDirection: StoryViewDirectionType.Previous,
})(dispatch, getState, null);
sinon.assert.calledWith(dispatch, {
type: 'stories/VIEW_STORY',
payload: {
currentIndex: 0,
messageId: storyId4,
numStories: 1,
storyViewMode: StoryViewModeType.MyStories,
unviewedStoryConversationIdsSorted: [],
},
});
});
});
describe('paging through collections of stories', () => {
function getViewedStoryData(
storyId: string,
conversationId?: string
conversationId?: UUIDStringType
): StoryDataType {
return {
...getStoryData(storyId, conversationId),
@ -407,11 +767,11 @@ describe('both/state/ducks/stories', () => {
}
it("goes to the next user's stories", () => {
const storyId1 = uuid();
const storyId2 = uuid();
const storyId3 = uuid();
const conversationId2 = uuid();
const conversationId1 = uuid();
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const conversationId2 = UUID.generate().toString();
const conversationId1 = UUID.generate().toString();
const getState = getStateFunction(
[
getViewedStoryData(storyId1, conversationId1),
@ -438,16 +798,17 @@ describe('both/state/ducks/stories', () => {
messageId: storyId2,
numStories: 2,
storyViewMode: StoryViewModeType.All,
unviewedStoryConversationIdsSorted: [],
},
});
});
it("goes to the prev user's stories", () => {
const storyId1 = uuid();
const storyId2 = uuid();
const storyId3 = uuid();
const conversationId1 = uuid();
const conversationId2 = uuid();
const storyId1 = UUID.generate().toString();
const storyId2 = UUID.generate().toString();
const storyId3 = UUID.generate().toString();
const conversationId1 = UUID.generate().toString();
const conversationId2 = UUID.generate().toString();
const getState = getStateFunction(
[
getViewedStoryData(storyId1, conversationId2),
@ -474,6 +835,7 @@ describe('both/state/ducks/stories', () => {
messageId: storyId1,
numStories: 2,
storyViewMode: StoryViewModeType.All,
unviewedStoryConversationIdsSorted: [],
},
});
});
@ -484,7 +846,7 @@ describe('both/state/ducks/stories', () => {
const { queueStoryDownload } = actions;
it('no attachment, no dispatch', async function test() {
const storyId = uuid();
const storyId = UUID.generate().toString();
const messageAttributes = getStoryMessage(storyId);
window.MessageController.register(storyId, messageAttributes);
@ -496,13 +858,13 @@ describe('both/state/ducks/stories', () => {
});
it('downloading, no dispatch', async function test() {
const storyId = uuid();
const storyId = UUID.generate().toString();
const messageAttributes = {
...getStoryMessage(storyId),
attachments: [
{
contentType: IMAGE_JPEG,
downloadJobId: uuid(),
downloadJobId: UUID.generate().toString(),
pending: true,
size: 0,
},
@ -518,7 +880,7 @@ describe('both/state/ducks/stories', () => {
});
it('downloaded, no dispatch', async function test() {
const storyId = uuid();
const storyId = UUID.generate().toString();
const messageAttributes = {
...getStoryMessage(storyId),
attachments: [
@ -540,7 +902,7 @@ describe('both/state/ducks/stories', () => {
});
it('downloaded, but unresolved, we should resolve the path', async function test() {
const storyId = uuid();
const storyId = UUID.generate().toString();
const attachment = {
contentType: IMAGE_JPEG,
path: 'image.jpg',
@ -620,7 +982,7 @@ describe('both/state/ducks/stories', () => {
});
it('not downloaded, queued for download', async function test() {
const storyId = uuid();
const storyId = UUID.generate().toString();
const messageAttributes = {
...getStoryMessage(storyId),
attachments: [

View file

@ -127,6 +127,7 @@ export enum StoryViewTargetType {
export enum StoryViewModeType {
All = 'All',
Hidden = 'Hidden',
MyStories = 'MyStories',
Single = 'Single',
Unread = 'Unread',
User = 'User',