Stories: Fix flickering while switching stories (#4149)
This commit is contained in:
parent
5b821bd350
commit
b94999c204
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user