Stories: Fix flickering while switching stories (#4149)

This commit is contained in:
Alexander Zinchuk 2024-01-18 18:18:36 +01:00
parent 5b821bd350
commit b94999c204
7 changed files with 153 additions and 58 deletions

View File

@ -15,11 +15,13 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../../config';
import { getSenderTitle, isUserId } from '../../global/helpers';
import {
selectChat, selectIsCurrentUserPremium,
selectChat,
selectIsCurrentUserPremium,
selectPeer,
selectPeerStories, selectPeerStory,
selectPeerStory,
selectPerformanceSettingsValue,
selectTabState, selectUser,
selectTabState,
selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
@ -864,8 +866,6 @@ function Story({
export default memo(withGlobal<OwnProps>((global, {
peerId,
storyId,
isPrivateStories,
isArchivedStories,
isReportModalOpen,
isDeleteModalOpen,
}): StateProps => {
@ -879,6 +879,7 @@ export default memo(withGlobal<OwnProps>((global, {
viewModal,
isPrivacyModalOpen,
isStealthModalOpen,
storyList,
},
forwardMessages: { storyId: forwardedStoryId },
premiumModal,
@ -886,7 +887,6 @@ export default memo(withGlobal<OwnProps>((global, {
mapModal,
} = tabState;
const { isOpen: isPremiumModalOpen } = premiumModal || {};
const { orderedIds, pinnedIds, archiveIds } = selectPeerStories(global, peerId) || {};
const story = selectPeerStory(global, peerId, storyId);
const shouldForcePause = Boolean(
viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
@ -904,7 +904,7 @@ export default memo(withGlobal<OwnProps>((global, {
peer: (user || chat)!,
forwardSender,
story,
orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds),
orderedIds: storyList?.storyIdsByPeerId[peerId],
isMuted,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
shouldForcePause,

View File

@ -26,10 +26,11 @@ interface OwnProps {
interface StateProps {
lastViewedId?: number;
origin?: StoryViewerOrigin;
storyIdsForViewer?: number[];
}
function StoryPreview({
peer, peerStories, lastViewedId, origin,
peer, peerStories, lastViewedId, storyIdsForViewer, origin,
}: OwnProps & StateProps) {
const { openStoryViewer, loadPeerSkippedStories } = getActions();
const lang = useLang();
@ -43,11 +44,13 @@ function StoryPreview({
orderedIds, lastReadId, byId,
} = peerStories;
const hasUnreadStories = orderedIds[orderedIds.length - 1] !== lastReadId;
const previewIndexId = lastViewedId ?? (hasUnreadStories ? (lastReadId ?? -1) : -1);
const resultId = byId[previewIndexId]?.id || orderedIds[0];
const previewIndexId = lastViewedId && storyIdsForViewer?.includes(lastViewedId)
? lastViewedId ?? (hasUnreadStories ? (lastReadId ?? -1) : -1)
: -1;
const resultId = byId[previewIndexId]?.id || storyIdsForViewer?.[0] || orderedIds[0];
return byId[resultId];
}, [lastViewedId, peerStories]);
}, [lastViewedId, peerStories, storyIdsForViewer]);
const isLoaded = story && 'content' in story;
@ -95,11 +98,13 @@ export default memo(withGlobal<OwnProps>((global, { peer }): StateProps => {
storyViewer: {
lastViewedByPeerIds,
origin,
storyList,
},
} = selectTabState(global);
return {
lastViewedId: peer?.id ? lastViewedByPeerIds?.[peer.id] : undefined,
origin,
storyIdsForViewer: peer?.id ? storyList?.storyIdsByPeerId[peer.id] : undefined,
};
})(StoryPreview));

View File

@ -158,45 +158,20 @@ function StorySlides({
return renderingPeerIds.indexOf(currentPeerId);
}, [currentPeerId, renderingPeerIds]);
// Handling the flipping of stories from a current user
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setRenderingPeerId(currentPeerId);
}, animationDuration);
return () => {
window.clearTimeout(timeoutId);
};
}, [animationDuration, currentPeerId]);
useEffect(() => {
let timeOutId: number | undefined;
if (renderingPeerId !== currentPeerId) {
timeOutId = window.setTimeout(() => {
setRenderingStoryId(currentStoryId);
}, animationDuration);
} else if (currentStoryId !== renderingStoryId) {
if (renderingPeerId === currentPeerId && currentStoryId !== renderingStoryId) {
setRenderingStoryId(currentStoryId);
}
return () => {
window.clearTimeout(timeOutId);
};
}, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId, animationDuration]);
}, [currentPeerId, currentStoryId, renderingPeerId, renderingStoryId]);
useEffect(() => {
let timeOutId: number | undefined;
if (prevPeerId && prevPeerId !== currentPeerId) {
setIsAnimating(true);
timeOutId = window.setTimeout(() => {
setIsAnimating(false);
}, animationDuration);
}
return () => {
setIsAnimating(false);
window.clearTimeout(timeOutId);
};
}, [prevPeerId, currentPeerId, setIsAnimating, animationDuration]);
@ -373,6 +348,21 @@ function StorySlides({
});
}, [currentPeerId, getIsAnimating, renderingPeerId, slideSizes, isMobile]);
const handleTransitionEnd = useLastCallback((event: React.TransitionEvent<HTMLDivElement>) => {
// It is `target` that is needed here, not `currentTarget`
const target = event.target as HTMLDivElement | null;
if (!target || !target.classList.contains(styles.activeSlide)) return;
if (renderingPeerId !== currentPeerId) {
setRenderingPeerId(currentPeerId);
setRenderingStoryId(currentStoryId);
} else if (currentStoryId !== renderingStoryId) {
setRenderingStoryId(currentStoryId);
}
setIsAnimating(false);
});
if (isMobile) {
return (
<div className={styles.wrapper} ref={containerRef}>
@ -461,6 +451,7 @@ function StorySlides({
className={styles.wrapper}
ref={containerRef}
style={`--story-viewer-scale: ${slideSizes.scale}`}
onTransitionEnd={handleTransitionEnd}
>
<div className={styles.fullSize} onClick={onClose} />
{renderingPeerIds.length > 1 && (
@ -480,15 +471,14 @@ function StorySlides({
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
storyViewer: {
peerId: currentPeerId, storyId: currentStoryId, isSinglePeer, isSingleStory, isPrivate, isArchive,
peerId: currentPeerId, storyId: currentStoryId, isSinglePeer, isSingleStory, isPrivate, isArchive, storyList,
},
} = selectTabState(global);
const { byPeerId, orderedPeerIds: { archived, active } } = global.stories;
const peer = currentPeerId ? selectPeer(global, currentPeerId) : undefined;
const { byPeerId, orderedPeerIds: { active } } = global.stories;
return {
byPeerId,
peerIds: peer?.areStoriesHidden ? archived : active,
peerIds: storyList?.peerIds ?? active,
currentPeerId,
currentStoryId,
isSinglePeer,

View File

@ -89,6 +89,7 @@
}
.slideAnimation {
/* Slide switching is made using the `onTransitionEnd` event, so don't remove the animation */
transition: transform 350ms ease-in-out !important;
}
@ -132,6 +133,14 @@
opacity: 1 !important;
visibility: visible;
}
.contentInner {
width: 100%;
}
.name {
width: calc(100% * var(--slide-translate-scale));
}
}
.slide {

View File

@ -14,6 +14,7 @@ import {
selectPeerFirstStoryId,
selectPeerFirstUnreadStoryId,
selectPeerStories,
selectStoryListForViewer,
selectTabState,
} from '../../selectors';
import { fetchChatByUsername } from '../api/chats';
@ -43,6 +44,9 @@ addActionHandler('openStoryViewer', async (global, actions, payload): Promise<vo
global = addStoriesForPeer(global, peerId, result.stories);
}
const storyList = tabState.storyViewer.storyList
|| selectStoryListForViewer(global, peerId, storyId, isSingleStory, isSinglePeer, isPrivate, isArchive);
global = updateTabState(global, {
storyViewer: {
...tabState.storyViewer,
@ -54,6 +58,7 @@ addActionHandler('openStoryViewer', async (global, actions, payload): Promise<vo
isSingleStory,
viewModal: undefined,
origin,
storyList,
},
}, tabId);
setGlobal(global);
@ -94,6 +99,7 @@ addActionHandler('closeStoryViewer', (global, actions, payload): ActionReturnTyp
isRibbonShown,
isArchivedRibbonShown,
lastViewedByPeerIds: undefined,
storyList: undefined,
},
}, tabId);
@ -134,15 +140,14 @@ addActionHandler('openPreviousStory', (global, actions, payload): ActionReturnTy
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const {
peerId, storyId, isSinglePeer, isSingleStory, isPrivate, isArchive,
peerId, storyId, isSinglePeer, isSingleStory, storyList,
} = tabState.storyViewer;
if (isSingleStory) {
if (isSingleStory || !storyList) {
actions.closeStoryViewer({ tabId });
return undefined;
}
const { orderedPeerIds: { active, archived } } = global.stories;
if (!peerId || !storyId) {
return undefined;
}
@ -153,9 +158,8 @@ addActionHandler('openPreviousStory', (global, actions, payload): ActionReturnTy
return undefined;
}
const orderedPeerIds = (peer.areStoriesHidden ? archived : active) ?? [];
const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'pinnedIds' : 'orderedIds';
const peerStoryIds = peerStories[storySourceProp] ?? [];
const { peerIds: orderedPeerIds, storyIdsByPeerId } = storyList;
const peerStoryIds = storyIdsByPeerId[peerId] ?? [];
const currentStoryIndex = peerStoryIds.indexOf(storyId);
let previousStoryIndex: number;
let previousPeerId: string;
@ -170,10 +174,10 @@ addActionHandler('openPreviousStory', (global, actions, payload): ActionReturnTy
}
previousPeerId = orderedPeerIds[previousPeerIdIndex];
previousStoryIndex = (selectPeerStories(global, previousPeerId)?.orderedIds.length || 1) - 1;
previousStoryIndex = (storyIdsByPeerId?.[previousPeerId]?.length || 1) - 1;
}
const previousStoryId = selectPeerStories(global, previousPeerId)?.[storySourceProp]?.[previousStoryIndex];
const previousStoryId = storyIdsByPeerId?.[previousPeerId]?.[previousStoryIndex];
if (!previousStoryId) {
return undefined;
}
@ -191,14 +195,13 @@ addActionHandler('openNextStory', (global, actions, payload): ActionReturnType =
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const {
peerId, storyId, isSinglePeer, isSingleStory, isPrivate, isArchive,
peerId, storyId, isSinglePeer, isSingleStory, storyList,
} = tabState.storyViewer;
if (isSingleStory) {
if (isSingleStory || !storyList) {
actions.closeStoryViewer({ tabId });
return undefined;
}
const { orderedPeerIds: { active, archived } } = global.stories;
if (!peerId || !storyId) {
return undefined;
}
@ -209,9 +212,8 @@ addActionHandler('openNextStory', (global, actions, payload): ActionReturnType =
return undefined;
}
const orderedPeerIds = (peer.areStoriesHidden ? archived : active) ?? [];
const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'pinnedIds' : 'orderedIds';
const peerStoryIds = peerStories[storySourceProp] ?? [];
const { peerIds: orderedPeerIds, storyIdsByPeerId } = storyList;
const peerStoryIds = storyIdsByPeerId[peerId] ?? [];
const currentStoryIndex = peerStoryIds.indexOf(storyId);
let nextStoryIndex: number;
let nextPeerId: string;
@ -230,7 +232,7 @@ addActionHandler('openNextStory', (global, actions, payload): ActionReturnType =
nextStoryIndex = 0;
}
const nextStoryId = selectPeerStories(global, nextPeerId)?.[storySourceProp]?.[nextStoryIndex];
const nextStoryId = storyIdsByPeerId?.[nextPeerId]?.[nextStoryIndex];
if (!nextStoryId) {
return undefined;
}

View File

@ -2,6 +2,7 @@ import type { ApiPeerStories, ApiTypeStory } from '../../api/types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { selectPeer } from './chats';
import { selectTabState } from './tabs';
export function selectCurrentViewedStory<T extends GlobalState>(
@ -56,3 +57,86 @@ export function selectPeerFirstStoryId<T extends GlobalState>(
) {
return selectPeerStories(global, peerId)?.orderedIds?.[0];
}
export function selectStoryListForViewer<T extends GlobalState>(
global: T,
peerId: string,
storyId?: number,
isSingleStory?: boolean,
isSinglePeer?: boolean,
isPrivate?: boolean,
isArchive?: boolean,
): {
peerIds: string[];
storyIdsByPeerId: Record<string, number[]>;
} | undefined {
const currentStoryId = storyId
|| selectPeerFirstUnreadStoryId(global, peerId)
|| selectPeerFirstStoryId(global, peerId);
if (!currentStoryId) {
return undefined;
}
if (isSingleStory) {
return {
peerIds: [peerId],
storyIdsByPeerId: { [peerId]: [currentStoryId] },
};
}
const peer = selectPeer(global, peerId);
const story = selectPeerStory(global, peerId, currentStoryId);
if (!peer || !story) {
return undefined;
}
const isUnread = (global.stories.byPeerId[peerId].lastReadId || 0) < story.id;
if (isSinglePeer) {
const storyIds = getPeerStoryIdsForViewer(global, peerId, isUnread, isArchive, isPrivate);
return storyIds?.length
? { peerIds: [peerId], storyIdsByPeerId: { [peerId]: storyIds } }
: undefined;
}
const { orderedPeerIds: { active, archived } } = global.stories;
const orderedPeerIds = (peer.areStoriesHidden ? archived : active) ?? [];
const peerIds: string[] = [];
const storyIdsByPeerId: Record<string, number[]> = {};
for (const currentPeerId of orderedPeerIds) {
const storyIds = getPeerStoryIdsForViewer(global, currentPeerId, isUnread, isArchive, isPrivate);
if (storyIds?.length) {
peerIds.push(currentPeerId);
storyIdsByPeerId[currentPeerId] = storyIds;
}
}
return peerIds.length ? { peerIds, storyIdsByPeerId } : undefined;
}
function getPeerStoryIdsForViewer<T extends GlobalState>(
global: T,
peerId: string,
isUnread?: boolean,
isArchive?: boolean,
isPrivate?: boolean,
): number[] | undefined {
const peerStories = selectPeerStories(global, peerId);
const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'pinnedIds' : 'orderedIds';
const storyIds = peerStories?.[storySourceProp];
if (!peerStories || !storyIds?.length) {
return undefined;
}
if (!peerStories.lastReadId || !isUnread) {
return storyIds.slice();
}
const lastReadIndex = storyIds.indexOf(peerStories.lastReadId);
return (storyIds.length > lastReadIndex + 1)
? storyIds.slice(lastReadIndex + 1)
: undefined;
}

View File

@ -383,6 +383,11 @@ export type TabState = {
isLoading?: boolean;
};
origin?: StoryViewerOrigin;
// Copy of story list for current view session
storyList?: {
peerIds: string[];
storyIdsByPeerId: Record<string, number[]>;
};
};
mediaViewer: {