596 lines
16 KiB
TypeScript
596 lines
16 KiB
TypeScript
import type {
|
|
ApiPeerStories,
|
|
ApiReaction,
|
|
ApiStealthMode,
|
|
ApiStory,
|
|
ApiStoryDeleted,
|
|
ApiStorySkipped,
|
|
ApiStoryViews,
|
|
ApiTypeStory,
|
|
ApiTypeStoryView,
|
|
} from '../../api/types';
|
|
import type { GlobalState, TabArgs } from '../types';
|
|
|
|
import { isUserId } from '../../util/entities/ids';
|
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
|
import { compareFields, unique } from '../../util/iteratees';
|
|
import { getServerTime } from '../../util/serverTime';
|
|
import { updateReactionCount } from '../helpers';
|
|
import {
|
|
selectIsChatWithSelf,
|
|
selectPeer, selectPeerStories, selectPeerStory, selectTabState, selectUser,
|
|
} from '../selectors';
|
|
import { updatePeer } from './peers';
|
|
import { updateTabState } from './tabs';
|
|
|
|
export function addStories<T extends GlobalState>(global: T, newStoriesByPeerId: Record<string, ApiPeerStories>): T {
|
|
const updatedByPeerId = Object.entries(newStoriesByPeerId).reduce((acc, [peerId, newPeerStories]) => {
|
|
if (!acc[peerId]) {
|
|
acc[peerId] = newPeerStories;
|
|
} else {
|
|
acc[peerId].byId = { ...acc[peerId].byId, ...newPeerStories.byId };
|
|
acc[peerId].orderedIds = unique(newPeerStories.orderedIds.concat(acc[peerId].orderedIds));
|
|
acc[peerId].profileIds = unique(newPeerStories.profileIds.concat(acc[peerId].profileIds)).sort((a, b) => b - a);
|
|
acc[peerId].lastUpdatedAt = newPeerStories.lastUpdatedAt;
|
|
acc[peerId].lastReadId = newPeerStories.lastReadId;
|
|
}
|
|
|
|
return acc;
|
|
}, global.stories.byPeerId);
|
|
|
|
global = {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: updatedByPeerId,
|
|
},
|
|
};
|
|
|
|
return updateOrderedStoriesPeerIds(global, Object.keys(newStoriesByPeerId));
|
|
}
|
|
|
|
export function addStoriesForPeer<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
newStories: Record<number, ApiTypeStory>,
|
|
newPinnedIds?: number[],
|
|
addToArchive?: boolean,
|
|
albumId?: number,
|
|
): T {
|
|
const {
|
|
byId, orderedIds, profileIds, archiveIds, pinnedIds, idsByAlbumId,
|
|
} = global.stories.byPeerId[peerId] || {};
|
|
const deletedIds = Object.keys(newStories).filter((id) => 'isDeleted' in newStories[Number(id)]).map(Number);
|
|
const updatedById = { ...byId, ...newStories };
|
|
let updatedOrderedIds = [...(orderedIds || [])];
|
|
let updatedArchiveIds = [...(archiveIds || [])];
|
|
const updatedProfileIds = unique(
|
|
[...(profileIds || [])].concat(Object.values(newStories).reduce((ids, story) => {
|
|
if (('isInProfile' in story && story.isInProfile)) {
|
|
ids.push(story.id);
|
|
}
|
|
|
|
return ids;
|
|
}, [] as number[])),
|
|
).sort((a, b) => b - a).filter((storyId) => !deletedIds.includes(storyId));
|
|
|
|
updatedOrderedIds = unique(Object.entries(newStories).reduce((acc, [storyId, story]) => {
|
|
if ('expireDate' in story && story.expireDate && story.expireDate > getServerTime()) {
|
|
acc.push(Number(storyId));
|
|
}
|
|
|
|
return acc;
|
|
}, updatedOrderedIds)).filter((storyId) => !deletedIds.includes(storyId));
|
|
|
|
if (addToArchive && selectIsChatWithSelf(global, peerId)) {
|
|
updatedArchiveIds = unique(updatedArchiveIds.concat(Object.keys(newStories).map(Number)))
|
|
.sort((a, b) => b - a)
|
|
.filter((storyId) => !deletedIds.includes(storyId));
|
|
}
|
|
|
|
const updatedIdsByAlbumId = { ...(idsByAlbumId || {}) };
|
|
if (albumId !== undefined) {
|
|
const newAlbumStoryIds = Object.keys(newStories).map(Number)
|
|
.filter((storyId) => !deletedIds.includes(storyId));
|
|
|
|
const existingAlbumData = updatedIdsByAlbumId[albumId];
|
|
const existingIds = existingAlbumData?.ids || [];
|
|
|
|
updatedIdsByAlbumId[albumId] = {
|
|
...updatedIdsByAlbumId[albumId],
|
|
ids: unique([...existingIds, ...newAlbumStoryIds]).sort((a, b) => b - a),
|
|
};
|
|
}
|
|
|
|
global = {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: {
|
|
...global.stories.byPeerId[peerId],
|
|
byId: updatedById,
|
|
orderedIds: updatedOrderedIds,
|
|
profileIds: updatedProfileIds,
|
|
pinnedIds: pinnedIds || newPinnedIds,
|
|
...(addToArchive && { archiveIds: updatedArchiveIds }),
|
|
...(albumId !== undefined && { idsByAlbumId: updatedIdsByAlbumId }),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
if (selectIsChatWithSelf(global, peerId)
|
|
|| selectUser(global, peerId)?.isContact
|
|
|| peerId === global.appConfig.storyChangelogUserId) {
|
|
global = updatePeerLastUpdatedAt(global, peerId);
|
|
global = updateOrderedStoriesPeerIds(global, [peerId]);
|
|
}
|
|
|
|
return global;
|
|
}
|
|
|
|
export function updateStoriesForPeer<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
peerStories: ApiPeerStories,
|
|
): T {
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: peerStories,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updatePeerStoriesFullyLoaded<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
isFullyLoaded: boolean,
|
|
isArchive?: boolean,
|
|
albumId?: number,
|
|
): T {
|
|
const { byPeerId } = global.stories;
|
|
const peerStories = byPeerId[peerId];
|
|
|
|
if (albumId !== undefined && peerStories?.idsByAlbumId?.[albumId]) {
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...byPeerId,
|
|
[peerId]: {
|
|
...peerStories,
|
|
idsByAlbumId: {
|
|
...peerStories.idsByAlbumId,
|
|
[albumId]: {
|
|
...peerStories.idsByAlbumId[albumId],
|
|
isFullyLoaded,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: {
|
|
...global.stories.byPeerId[peerId],
|
|
[isArchive ? 'isArchiveFullyLoaded' : 'isFullyLoaded']: isFullyLoaded,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updateLastReadStoryForPeer<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
lastReadId: number,
|
|
): T {
|
|
const { orderedIds } = selectPeerStories(global, peerId) || {};
|
|
if (!orderedIds) {
|
|
return global;
|
|
}
|
|
|
|
if (lastReadId >= orderedIds[orderedIds.length - 1]) {
|
|
global = updatePeer(global, peerId, {
|
|
hasUnreadStories: false,
|
|
});
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: {
|
|
...global.stories.byPeerId[peerId],
|
|
lastReadId,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updateLastViewedStoryForPeer<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
lastViewedId: number,
|
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
): T {
|
|
const { orderedIds } = selectPeerStories(global, peerId) || {};
|
|
if (!orderedIds || !orderedIds.includes(lastViewedId)) {
|
|
return global;
|
|
}
|
|
|
|
const { storyViewer } = selectTabState(global, tabId);
|
|
|
|
return updateTabState(global, {
|
|
storyViewer: {
|
|
...storyViewer,
|
|
lastViewedByPeerId: {
|
|
...storyViewer.lastViewedByPeerId,
|
|
[peerId]: lastViewedId,
|
|
},
|
|
},
|
|
}, tabId);
|
|
}
|
|
|
|
export function updatePeersWithStories<T extends GlobalState>(
|
|
global: T,
|
|
storiesByPeerId: Record<string, ApiPeerStories>,
|
|
): T {
|
|
Object.entries(storiesByPeerId).forEach(([peerId, { lastReadId, orderedIds }]) => {
|
|
const peer = selectPeer(global, peerId);
|
|
if (!peer) return;
|
|
|
|
global = updatePeer(global, peerId, {
|
|
hasStories: true,
|
|
hasUnreadStories: !lastReadId
|
|
|| Boolean(lastReadId && lastReadId < (peer.maxStoryId || orderedIds[orderedIds.length - 1])),
|
|
});
|
|
});
|
|
|
|
return global;
|
|
}
|
|
|
|
export function updateStoryViews<T extends GlobalState>(
|
|
global: T,
|
|
storyId: number,
|
|
views: ApiTypeStoryView[],
|
|
nextOffset?: string,
|
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
): T {
|
|
const tabState = selectTabState(global, tabId);
|
|
const { viewModal } = tabState.storyViewer;
|
|
const newViews = viewModal?.storyId === storyId && viewModal.views ? [
|
|
...viewModal.views,
|
|
...views,
|
|
] : views;
|
|
|
|
global = updateStoryViewsLoading(global, false, tabId);
|
|
|
|
return updateTabState(global, {
|
|
storyViewer: {
|
|
...tabState.storyViewer,
|
|
viewModal: {
|
|
...viewModal,
|
|
storyId,
|
|
views: newViews,
|
|
nextOffset,
|
|
isLoading: false,
|
|
},
|
|
},
|
|
}, tabId);
|
|
}
|
|
|
|
export function updateStoryViewsLoading<T extends GlobalState>(
|
|
global: T,
|
|
isLoading: boolean,
|
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
): T {
|
|
const tabState = selectTabState(global, tabId);
|
|
const { viewModal } = tabState.storyViewer;
|
|
if (!viewModal) return global;
|
|
|
|
return updateTabState(global, {
|
|
storyViewer: {
|
|
...tabState.storyViewer,
|
|
viewModal: {
|
|
...viewModal,
|
|
isLoading,
|
|
},
|
|
},
|
|
}, tabId);
|
|
}
|
|
|
|
export function removePeerStory<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
storyId: number,
|
|
): T {
|
|
const {
|
|
orderedIds, profileIds, lastReadId, byId,
|
|
} = selectPeerStories(global, peerId) || { orderedIds: [] as number[], profileIds: [] as number[] };
|
|
|
|
const newOrderedIds = orderedIds.filter((id) => id !== storyId);
|
|
const newProfileIds = profileIds.filter((id) => id !== storyId);
|
|
const lastStoryId = newOrderedIds.length ? orderedIds[orderedIds.length - 1] : undefined;
|
|
|
|
const previousStoryId = orderedIds[orderedIds.indexOf(storyId) - 1];
|
|
const newLastReadId = lastReadId === storyId ? previousStoryId : lastReadId;
|
|
|
|
const newById = {
|
|
...byId,
|
|
[storyId]: { id: storyId, peerId, isDeleted: true } as ApiStoryDeleted,
|
|
};
|
|
const lastUpdatedAt = lastStoryId ? (newById[lastStoryId] as ApiStory | undefined)?.date : undefined;
|
|
const hasStories = Boolean(newOrderedIds.length);
|
|
|
|
global = updatePeer(global, peerId, {
|
|
hasStories,
|
|
hasUnreadStories: Boolean(hasStories && lastReadId && lastStoryId && lastReadId < lastStoryId),
|
|
});
|
|
|
|
global = updateStoriesForPeer(global, peerId, {
|
|
byId: newById,
|
|
orderedIds: newOrderedIds,
|
|
profileIds: newProfileIds,
|
|
lastUpdatedAt,
|
|
lastReadId: newLastReadId,
|
|
});
|
|
|
|
Object.values(global.byTabId).forEach((tab) => {
|
|
if (tab.storyViewer.lastViewedByPeerId && tab.storyViewer.lastViewedByPeerId[peerId] === storyId) {
|
|
global = updateLastViewedStoryForPeer(global, peerId, previousStoryId, tab.id);
|
|
}
|
|
});
|
|
|
|
if (!hasStories) {
|
|
global = {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
orderedPeerIds: {
|
|
active: global.stories.orderedPeerIds.active.filter((id) => id !== peerId),
|
|
archived: global.stories.orderedPeerIds.archived.filter((id) => id !== peerId),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return global;
|
|
}
|
|
|
|
export function updateSentStoryReaction<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
storyId: number,
|
|
reaction: ApiReaction | undefined,
|
|
): T {
|
|
const story = selectPeerStory(global, peerId, storyId);
|
|
if (!story || !('content' in story)) return global;
|
|
|
|
const { views } = story;
|
|
const reactionsCount = views?.reactionsCount || 0;
|
|
const hasReaction = views?.reactions?.some((r) => r.chosenOrder !== undefined);
|
|
const reactions = updateReactionCount(views?.reactions || [], [reaction].filter(Boolean));
|
|
|
|
const countDiff = !reaction ? -1 : hasReaction ? 0 : 1;
|
|
const newReactionsCount = reactionsCount + countDiff;
|
|
|
|
global = updatePeerStory(global, peerId, storyId, {
|
|
sentReaction: reaction,
|
|
views: {
|
|
...views,
|
|
reactionsCount: newReactionsCount,
|
|
reactions,
|
|
},
|
|
});
|
|
|
|
return global;
|
|
}
|
|
|
|
export function updatePeerStory<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
storyId: number,
|
|
storyUpdate: Partial<ApiStory>,
|
|
): T {
|
|
const peerStories = selectPeerStories(global, peerId) || {
|
|
byId: {}, orderedIds: [], profileIds: [], archiveIds: [],
|
|
};
|
|
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: {
|
|
...peerStories,
|
|
byId: {
|
|
...peerStories.byId,
|
|
[storyId]: {
|
|
...peerStories.byId[storyId],
|
|
...storyUpdate,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updatePeerStoryViews<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
storyId: number,
|
|
viewsUpdate: Partial<ApiStoryViews>,
|
|
): T {
|
|
const story = selectPeerStory(global, peerId, storyId);
|
|
if (!story || !('content' in story)) return global;
|
|
|
|
const { views } = story;
|
|
|
|
return updatePeerStory(global, peerId, storyId, {
|
|
views: {
|
|
...views,
|
|
...viewsUpdate,
|
|
},
|
|
});
|
|
}
|
|
|
|
export function updatePeerProfileStory<T extends GlobalState>(
|
|
global: T,
|
|
peerId: string,
|
|
storyId: number,
|
|
isInProfile?: boolean,
|
|
): T {
|
|
const peerStories = selectPeerStories(global, peerId) || {
|
|
byId: {}, orderedIds: [], profileIds: [], archiveIds: [],
|
|
};
|
|
|
|
const newProfileIds = isInProfile
|
|
? unique(peerStories.profileIds.concat(storyId)).sort((a, b) => b - a)
|
|
: peerStories.profileIds.filter((id) => storyId !== id);
|
|
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: {
|
|
...peerStories,
|
|
profileIds: newProfileIds,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updatePeerStoriesHidden<T extends GlobalState>(global: T, peerId: string, areHidden: boolean) {
|
|
const peer = selectPeer(global, peerId);
|
|
if (!peer) return global;
|
|
|
|
const currentState = peer.areStoriesHidden;
|
|
if (currentState === areHidden) return global; // `updateOrderedStoriesPeerIds` is computationally expensive
|
|
|
|
global = updatePeer(global, peerId, {
|
|
areStoriesHidden: areHidden,
|
|
});
|
|
|
|
return updateOrderedStoriesPeerIds(global, [peerId]);
|
|
}
|
|
|
|
function updateOrderedStoriesPeerIds<T extends GlobalState>(global: T, updatePeerIds: string[]): T {
|
|
const { currentUserId, stories: { byPeerId, orderedPeerIds } } = global;
|
|
|
|
const allPeerIds = orderedPeerIds.active.concat(orderedPeerIds.archived).concat(updatePeerIds);
|
|
const newOrderedPeerIds = allPeerIds.reduce<{ active: string[]; archived: string[] }>((acc, peerId) => {
|
|
if (!byPeerId[peerId]?.orderedIds?.length) return acc;
|
|
const peer = selectPeer(global, peerId);
|
|
|
|
if (peer?.areStoriesHidden) {
|
|
acc.archived.push(peerId);
|
|
} else {
|
|
acc.active.push(peerId);
|
|
}
|
|
|
|
return acc;
|
|
}, { active: [], archived: [] });
|
|
|
|
function compare(peerIdA: string, peerIdB: string) {
|
|
const peerA = selectPeer(global, peerIdA)!;
|
|
const peerB = selectPeer(global, peerIdB)!;
|
|
|
|
const diffCurrentUser = compareFields(currentUserId === peerIdA, currentUserId === peerIdB);
|
|
if (diffCurrentUser) return diffCurrentUser;
|
|
|
|
const { lastUpdatedAt: luaA = 0, orderedIds: orderedA, lastReadId: lriA = 0 } = byPeerId[peerIdA] || {};
|
|
const hasUnreadA = lriA < orderedA?.[orderedA.length - 1];
|
|
const { lastUpdatedAt: luaB = 0, orderedIds: orderedB, lastReadId: lriB = 0 } = byPeerId[peerIdB] || {};
|
|
const hasUnreadB = lriB < orderedB?.[orderedB.length - 1];
|
|
|
|
const diffUnread = compareFields(hasUnreadA, hasUnreadB);
|
|
if (diffUnread) return diffUnread;
|
|
|
|
const diffPremium = compareFields('isPremium' in peerA, 'isPremium' in peerB);
|
|
if (diffPremium) return diffPremium;
|
|
|
|
const diffType = compareFields(isUserId(peerIdA), isUserId(peerIdB));
|
|
if (diffType) return diffType;
|
|
|
|
return compareFields(luaA, luaB);
|
|
}
|
|
|
|
newOrderedPeerIds.archived = unique(newOrderedPeerIds.archived)
|
|
.filter((peerId) => byPeerId[peerId]?.orderedIds?.length)
|
|
.sort(compare);
|
|
newOrderedPeerIds.active = unique(newOrderedPeerIds.active)
|
|
.filter((peerId) => byPeerId[peerId]?.orderedIds?.length)
|
|
.sort(compare);
|
|
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
orderedPeerIds: newOrderedPeerIds,
|
|
},
|
|
};
|
|
}
|
|
|
|
function updatePeerLastUpdatedAt<T extends GlobalState>(global: T, peerId: string): T {
|
|
const peerStories = global.stories.byPeerId[peerId];
|
|
const lastUpdatedAt = peerStories.orderedIds.reduce<number | undefined>((acc, storyId) => {
|
|
const { date } = peerStories.byId[storyId] as ApiStorySkipped || {};
|
|
if (date && (!acc || acc < date)) {
|
|
acc = date;
|
|
}
|
|
|
|
return acc;
|
|
}, undefined);
|
|
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
byPeerId: {
|
|
...global.stories.byPeerId,
|
|
[peerId]: {
|
|
...peerStories,
|
|
lastUpdatedAt,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updateStealthMode<T extends GlobalState>(
|
|
global: T,
|
|
stealthMode: ApiStealthMode,
|
|
): T {
|
|
return {
|
|
...global,
|
|
stories: {
|
|
...global.stories,
|
|
stealthMode,
|
|
},
|
|
};
|
|
}
|