Story Viewer: Ghost animation (#3765)
This commit is contained in:
parent
3a56c041cd
commit
cd1f293e4b
@ -5,6 +5,7 @@ import { getActions } from '../../global';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { StoryViewerOrigin } from '../../types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import { IS_TEST } from '../../config';
|
||||
@ -16,6 +17,7 @@ import {
|
||||
isUserId,
|
||||
isChatWithRepliesBot,
|
||||
isDeletedUser,
|
||||
getUserStoryHtmlId,
|
||||
} from '../../global/helpers';
|
||||
import { getFirstLetters } from '../../util/textFormat';
|
||||
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
|
||||
@ -51,6 +53,7 @@ type OwnProps = {
|
||||
withStory?: boolean;
|
||||
withStoryGap?: boolean;
|
||||
withStorySolid?: boolean;
|
||||
storyViewerOrigin?: StoryViewerOrigin;
|
||||
storyViewerMode?: 'full' | 'single-user' | 'disabled';
|
||||
loopIndefinitely?: boolean;
|
||||
noPersonalPhoto?: boolean;
|
||||
@ -69,6 +72,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
withStory,
|
||||
withStoryGap,
|
||||
withStorySolid,
|
||||
storyViewerOrigin,
|
||||
storyViewerMode = 'single-user',
|
||||
loopIndefinitely,
|
||||
noPersonalPhoto,
|
||||
@ -220,7 +224,11 @@ const Avatar: FC<OwnProps> = ({
|
||||
if (withStory && storyViewerMode !== 'disabled' && user?.hasStories) {
|
||||
e.stopPropagation();
|
||||
|
||||
openStoryViewer({ userId: user.id, isSingleUser: storyViewerMode === 'single-user' });
|
||||
openStoryViewer({
|
||||
userId: user.id,
|
||||
isSingleUser: storyViewerMode === 'single-user',
|
||||
origin: storyViewerOrigin,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -233,6 +241,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={fullClassName}
|
||||
id={peer?.id && withStory ? getUserStoryHtmlId(peer.id) : undefined}
|
||||
data-test-sender-id={IS_TEST ? peer?.id : undefined}
|
||||
aria-label={typeof content === 'string' ? author : undefined}
|
||||
onClick={handleClick}
|
||||
|
||||
@ -5,13 +5,10 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import type {
|
||||
ApiUser, ApiTypingStatus, ApiUserStatus, ApiChatMember,
|
||||
} from '../../api/types';
|
||||
import type { StoryViewerOrigin } from '../../types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import {
|
||||
selectChatMessages,
|
||||
selectUser,
|
||||
selectUserStatus,
|
||||
} from '../../global/selectors';
|
||||
import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors';
|
||||
import { getMainUsername, getUserStatus, isUserOnline } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from './helpers/renderText';
|
||||
@ -39,6 +36,7 @@ type OwnProps = {
|
||||
withStory?: boolean;
|
||||
withFullInfo?: boolean;
|
||||
withUpdatingStatus?: boolean;
|
||||
storyViewerOrigin?: StoryViewerOrigin;
|
||||
noEmojiStatus?: boolean;
|
||||
emojiStatusSize?: number;
|
||||
noStatusOrTyping?: boolean;
|
||||
@ -77,6 +75,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
adminMember,
|
||||
ripple,
|
||||
onEmojiStatusClick,
|
||||
storyViewerOrigin,
|
||||
}) => {
|
||||
const {
|
||||
loadFullUser,
|
||||
@ -187,6 +186,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
peer={user}
|
||||
isSavedMessages={isSavedMessages}
|
||||
withStory={withStory}
|
||||
storyViewerOrigin={storyViewerOrigin}
|
||||
storyViewerMode="single-user"
|
||||
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { StoryViewerOrigin } from '../../../types';
|
||||
import type { ChatAnimationTypes } from './hooks';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiFormattedText,
|
||||
@ -13,7 +16,6 @@ import type {
|
||||
ApiUser,
|
||||
ApiUserStatus,
|
||||
} from '../../../api/types';
|
||||
import type { ChatAnimationTypes } from './hooks';
|
||||
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
@ -49,7 +51,6 @@ import useSelectorSignal from '../../../hooks/useSelectorSignal';
|
||||
import useChatContextActions from '../../../hooks/useChatContextActions';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useChatListEntry from './hooks/useChatListEntry';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
@ -265,6 +266,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSavedMessages={user?.isSelf}
|
||||
withStory={user && !user?.isSelf}
|
||||
withStoryGap={isAvatarOnlineShown}
|
||||
storyViewerOrigin={StoryViewerOrigin.ChatList}
|
||||
storyViewerMode="single-user"
|
||||
/>
|
||||
<div className="avatar-badge-wrapper">
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { useCallback, useMemo, memo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser, ApiUserStatus } from '../../../api/types';
|
||||
import { StoryViewerOrigin } from '../../../types';
|
||||
|
||||
import { filterUsersByName, sortUserIds } from '../../../global/helpers';
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
@ -75,7 +76,14 @@ const ContactList: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleClick(id)}
|
||||
>
|
||||
<PrivateChatInfo userId={id} forceShowSelf avatarSize="large" withStory ripple={!isMobile} />
|
||||
<PrivateChatInfo
|
||||
userId={id}
|
||||
forceShowSelf
|
||||
avatarSize="large"
|
||||
withStory
|
||||
storyViewerOrigin={StoryViewerOrigin.ChatList}
|
||||
ripple={!isMobile}
|
||||
/>
|
||||
</ListItem>
|
||||
))
|
||||
) : viewportIds && !viewportIds.length ? (
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||
import { StoryViewerOrigin } from '../../../types';
|
||||
|
||||
import useChatContextActions from '../../../hooks/useChatContextActions';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
@ -85,7 +86,13 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
||||
buttonRef={buttonRef}
|
||||
>
|
||||
{isUserId(chatId) ? (
|
||||
<PrivateChatInfo userId={chatId} withUsername={withUsername} withStory avatarSize="large" />
|
||||
<PrivateChatInfo
|
||||
userId={chatId}
|
||||
withUsername={withUsername}
|
||||
withStory
|
||||
avatarSize="large"
|
||||
storyViewerOrigin={StoryViewerOrigin.SearchResult}
|
||||
/>
|
||||
) : (
|
||||
<GroupChatInfo chatId={chatId} withUsername={withUsername} avatarSize="large" />
|
||||
)}
|
||||
|
||||
@ -10,6 +10,7 @@ import type { Signal } from '../../util/signals';
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiTypingStatus, ApiUser,
|
||||
} from '../../api/types';
|
||||
import { StoryViewerOrigin } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import {
|
||||
@ -22,7 +23,12 @@ import {
|
||||
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
} from '../../config';
|
||||
import {
|
||||
getChatTitle, getMessageKey, getSenderTitle, isChatChannel, isChatSuperGroup, isUserId,
|
||||
getChatTitle,
|
||||
getMessageKey,
|
||||
getSenderTitle,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectAllowedMessageActions,
|
||||
@ -379,6 +385,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
withMediaViewer
|
||||
withStory={!isChatWithSelf}
|
||||
withUpdatingStatus
|
||||
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
|
||||
emojiStatusSize={EMOJI_STATUS_SIZE}
|
||||
noRtl
|
||||
onEmojiStatusClick={handleStatusClick}
|
||||
|
||||
@ -8,20 +8,19 @@ import type { ApiStory, ApiTypeStory, ApiUser } from '../../api/types';
|
||||
import type { IDimensions } from '../../global/types';
|
||||
import type { Signal } from '../../util/signals';
|
||||
|
||||
import { ApiMediaFormat, MAIN_THREAD_ID } from '../../api/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import {
|
||||
getStoryMediaHash, getUserFirstOrLastName, hasMessageText,
|
||||
} from '../../global/helpers';
|
||||
import { getUserFirstOrLastName } from '../../global/helpers';
|
||||
import { formatRelativeTime } from '../../util/dateFormat';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
import { selectChat, selectIsCurrentUserPremium, selectTabState } from '../../global/selectors';
|
||||
import {
|
||||
selectChat, selectTabState, selectUserStory, selectUserStories, selectIsCurrentUserPremium,
|
||||
} from '../../global/selectors';
|
||||
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
||||
|
||||
import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useStoryPreloader from './hooks/useStoryPreloader';
|
||||
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
@ -34,6 +33,7 @@ import useLongPress from '../../hooks/useLongPress';
|
||||
import useUnsupportedMedia from '../../hooks/media/useUnsupportedMedia';
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
import { useStoryProps } from './hooks/useStoryProps';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import Avatar from '../common/Avatar';
|
||||
@ -139,9 +139,22 @@ function Story({
|
||||
const [isPausedByLongPress, markIsPausedByLongPress, unmarkIsPausedByLongPress] = useFlag(false);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const {
|
||||
isDeletedStory,
|
||||
hasText,
|
||||
thumbnail,
|
||||
previewBlobUrl,
|
||||
isVideo,
|
||||
noSound,
|
||||
fullMediaData,
|
||||
altMediaHash,
|
||||
altMediaData,
|
||||
hasFullData,
|
||||
hasThumb,
|
||||
} = useStoryProps(story);
|
||||
|
||||
const isLoadedStory = story && 'content' in story;
|
||||
const isDeletedStory = story && 'isDeleted' in story;
|
||||
const hasText = isLoadedStory ? hasMessageText(story) : false;
|
||||
|
||||
const canPinToProfile = useCurrentOrPrev(
|
||||
isSelf && isLoadedStory ? !story.isPinned : undefined,
|
||||
true,
|
||||
@ -159,6 +172,7 @@ function Story({
|
||||
&& userId !== storyChangelogUserId
|
||||
&& user?.usernames?.length,
|
||||
);
|
||||
|
||||
const canShare = Boolean(
|
||||
isLoadedStory
|
||||
&& story.isPublic
|
||||
@ -167,26 +181,6 @@ function Story({
|
||||
&& !isCaptionExpanded,
|
||||
);
|
||||
|
||||
let thumbnail: string | undefined;
|
||||
if (isLoadedStory) {
|
||||
if (story.content.photo?.thumbnail) {
|
||||
thumbnail = story.content.photo.thumbnail.dataUri;
|
||||
}
|
||||
if (story.content.video?.thumbnail?.dataUri) {
|
||||
thumbnail = story.content.video.thumbnail.dataUri;
|
||||
}
|
||||
}
|
||||
|
||||
const previewHash = isLoadedStory ? getStoryMediaHash(story) : undefined;
|
||||
const previewBlobUrl = useMedia(previewHash);
|
||||
const isVideo = Boolean(isLoadedStory && story.content.video);
|
||||
const noSound = isLoadedStory && story.content.video?.noSound;
|
||||
const fullMediaHash = isLoadedStory ? getStoryMediaHash(story, 'full') : undefined;
|
||||
const fullMediaData = useMedia(fullMediaHash, !story, isVideo ? ApiMediaFormat.Progressive : ApiMediaFormat.BlobUrl);
|
||||
const altMediaHash = isVideo && isLoadedStory ? getStoryMediaHash(story, 'full', true) : undefined;
|
||||
const altMediaData = useMedia(altMediaHash, !story, ApiMediaFormat.Progressive);
|
||||
|
||||
const hasFullData = Boolean(fullMediaData || altMediaData);
|
||||
const canPlayStory = Boolean(
|
||||
hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded
|
||||
&& !isPausedBySpacebar && !isPausedByLongPress,
|
||||
@ -199,7 +193,6 @@ function Story({
|
||||
transitionClassNames: mediaTransitionClassNames,
|
||||
} = useShowTransition(Boolean(fullMediaData));
|
||||
|
||||
const hasThumb = !previewBlobUrl && !hasFullData;
|
||||
const thumbRef = useCanvasBlur(thumbnail, !hasThumb);
|
||||
const previewTransitionClassNames = useMediaTransition(previewBlobUrl);
|
||||
|
||||
@ -777,10 +770,8 @@ export default memo(withGlobal<OwnProps>((global, {
|
||||
premiumModal,
|
||||
} = tabState;
|
||||
const { isOpen: isPremiumModalOpen } = premiumModal || {};
|
||||
const {
|
||||
byId, orderedIds, pinnedIds, archiveIds,
|
||||
} = global.stories.byUserId[userId] || {};
|
||||
const story = byId && storyId ? byId[storyId] : undefined;
|
||||
const { orderedIds, pinnedIds, archiveIds } = selectUserStories(global, userId) || {};
|
||||
const story = selectUserStory(global, userId, storyId);
|
||||
const shouldForcePause = Boolean(
|
||||
storyIdSeenBy || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
|
||||
|| isPremiumModalOpen || isDeleteModalOpen,
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiTypeStory, ApiUser, ApiUserStories } from '../../api/types';
|
||||
import type { StoryViewerOrigin } from '../../types';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
@ -19,10 +20,11 @@ interface OwnProps {
|
||||
|
||||
interface StateProps {
|
||||
lastViewedId?: number;
|
||||
origin?: StoryViewerOrigin;
|
||||
}
|
||||
|
||||
function StoryPreview({
|
||||
user, userStories, lastViewedId,
|
||||
user, userStories, lastViewedId, origin,
|
||||
}: OwnProps & StateProps) {
|
||||
const { openStoryViewer, loadUserSkippedStories } = getActions();
|
||||
|
||||
@ -61,7 +63,7 @@ function StoryPreview({
|
||||
return (
|
||||
<div
|
||||
className={styles.slideInner}
|
||||
onClick={() => { openStoryViewer({ userId: story.userId, storyId: story.id }); }}
|
||||
onClick={() => { openStoryViewer({ userId: story.userId, storyId: story.id, origin }); }}
|
||||
>
|
||||
{thumbUrl && (
|
||||
<img src={thumbUrl} alt="" className={styles.media} draggable={false} />
|
||||
@ -83,10 +85,12 @@ export default memo(withGlobal<OwnProps>((global, { user }): StateProps => {
|
||||
const {
|
||||
storyViewer: {
|
||||
lastViewedByUserIds,
|
||||
origin,
|
||||
},
|
||||
} = selectTabState(global);
|
||||
|
||||
return {
|
||||
lastViewedId: user?.id ? lastViewedByUserIds?.[user.id] : undefined,
|
||||
origin,
|
||||
};
|
||||
})(StoryPreview));
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiUser } from '../../api/types';
|
||||
import { StoryViewerOrigin } from '../../types';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { getUserFirstOrLastName } from '../../global/helpers';
|
||||
@ -62,7 +63,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (isContextMenuOpen) return;
|
||||
|
||||
openStoryViewer({ userId: user.id });
|
||||
openStoryViewer({ userId: user.id, origin: StoryViewerOrigin.StoryRibbon });
|
||||
});
|
||||
|
||||
const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
@ -103,6 +104,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
|
||||
<Avatar
|
||||
peer={user}
|
||||
withStory
|
||||
storyViewerOrigin={StoryViewerOrigin.StoryRibbon}
|
||||
storyViewerMode="full"
|
||||
/>
|
||||
<div className={buildClassName(styles.name, user.hasUnreadStories && styles.name_hasUnreadStory)}>
|
||||
|
||||
@ -8,15 +8,15 @@ import type { ApiUserStories } from '../../api/types';
|
||||
import { IS_FIREFOX, IS_SAFARI } from '../../util/windowEnvironment';
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import { selectIsStoryViewerOpen, selectTabState, selectUser } from '../../global/selectors';
|
||||
import { calculateOffsetX, calculateSlideSizes } from './helpers/dimensions';
|
||||
import { calculateOffsetX } from './helpers/dimensions';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useWindowSize from '../../hooks/useWindowSize';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useSignal from '../../hooks/useSignal';
|
||||
import { useSlideSizes } from './hooks/useSlideSizes';
|
||||
|
||||
import Story from './Story';
|
||||
import StoryPreview from './StoryPreview';
|
||||
@ -59,10 +59,8 @@ function StorySlides({
|
||||
const renderingIsPrivate = useCurrentOrPrev(isPrivate, true);
|
||||
const renderingIsSingleUser = useCurrentOrPrev(isSingleUser, true);
|
||||
const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true);
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
const slideSizes = useMemo(() => {
|
||||
return calculateSlideSizes(windowWidth, windowHeight);
|
||||
}, [windowWidth, windowHeight]);
|
||||
const slideSizes = useSlideSizes();
|
||||
|
||||
const rendersRef = useRef<Record<string, { current: HTMLDivElement | null }>>({});
|
||||
const [getIsAnimating, setIsAnimating] = useSignal(false);
|
||||
|
||||
|
||||
@ -19,11 +19,8 @@
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
:global(.opacity-transition) {
|
||||
transition: opacity 350ms;
|
||||
@media (max-width: 600px) {
|
||||
transition: none;
|
||||
}
|
||||
&:global(.opacity-transition) {
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
|
||||
:global(.text-entity-link) {
|
||||
@ -35,18 +32,6 @@
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
:global(body.enable-symbol-menu-transforms) & {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
:global(body.is-symbol-menu-open) & {
|
||||
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height))), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fullSize, .backdrop, .captionBackdrop {
|
||||
@ -63,6 +48,7 @@
|
||||
|
||||
.backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.backdropNonInteractive {
|
||||
@ -78,10 +64,10 @@
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
top: 1.125rem;
|
||||
top: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +80,7 @@
|
||||
overflow: hidden;
|
||||
transform: translateX(-50%);
|
||||
max-width: calc(73.5rem * var(--story-viewer-scale));
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
max-width: 100%;
|
||||
@ -101,7 +88,7 @@
|
||||
}
|
||||
|
||||
.slideAnimation {
|
||||
transition: transform 350ms ease-in-out;
|
||||
transition: transform 350ms ease-in-out !important;
|
||||
}
|
||||
|
||||
.slideAnimationToActive {
|
||||
@ -145,8 +132,7 @@
|
||||
calc(var(--slide-x, -50%) - var(--slide-translate-x, 0px)),
|
||||
calc(-50% - var(--slide-translate-y, 0px)),
|
||||
0
|
||||
)
|
||||
scale(var(--slide-translate-scale, 1));
|
||||
) scale(var(--slide-translate-scale, 1));
|
||||
transform-origin: 0 50%;
|
||||
|
||||
border-radius: var(--border-radius-default-small);
|
||||
@ -173,6 +159,7 @@
|
||||
|
||||
.slidePreview {
|
||||
overflow: hidden;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
@ -187,7 +174,12 @@
|
||||
}
|
||||
|
||||
&.slideAnimationToActive::before {
|
||||
transition: opacity 350ms ease-in-out;
|
||||
transition: opacity 350ms ease-in-out !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.root:global(.not-open) &,
|
||||
:global(body.ghost-animating) & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@ -224,7 +216,7 @@
|
||||
|
||||
@media (min-width: 600.001px) {
|
||||
&.slideAnimationFromActive::before {
|
||||
transition: opacity 350ms ease-in-out;
|
||||
transition: opacity 350ms ease-in-out !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -253,6 +245,10 @@
|
||||
height: calc(100% - 4rem) !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:global(body.ghost-animating) & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
@ -272,6 +268,10 @@
|
||||
border-radius: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(body.ghost-animating) .activeSlide & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -317,7 +317,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.storyHeader {
|
||||
// Shared styles for the header that are also used in ghost animation
|
||||
@mixin story-header {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
content: "";
|
||||
@ -330,6 +331,14 @@
|
||||
border-radius: var(--border-radius-default-small) var(--border-radius-default-small) 0 0;
|
||||
}
|
||||
|
||||
.storyHeader {
|
||||
@include story-header;
|
||||
|
||||
:global(body.ghost-animating) & {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.storyIndicators {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@ -459,7 +468,7 @@
|
||||
|
||||
.caption {
|
||||
position: absolute;
|
||||
bottom: 3.5rem;
|
||||
bottom: calc(3.5rem - 1px);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@ -690,3 +699,35 @@
|
||||
text-align: center;
|
||||
margin-block: auto;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
transition: transform 200ms ease;
|
||||
border-radius: var(--border-radius-default-small);
|
||||
|
||||
&:before {
|
||||
@include story-header;
|
||||
}
|
||||
}
|
||||
|
||||
.ghost2 {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: transform 200ms ease, opacity 200ms ease;
|
||||
}
|
||||
|
||||
.ghostImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
object-fit: cover;
|
||||
@media (max-width: 600px) {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,13 +3,28 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import { selectIsStoryViewerOpen, selectTabState } from '../../global/selectors';
|
||||
import type { ApiTypeStory } from '../../api/types';
|
||||
import type { StoryViewerOrigin } from '../../types';
|
||||
|
||||
import {
|
||||
selectIsStoryViewerOpen,
|
||||
selectTabState,
|
||||
selectUserStory,
|
||||
selectPerformanceSettingsValue,
|
||||
} from '../../global/selectors';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useStoryProps } from './hooks/useStoryProps';
|
||||
import { useSlideSizes } from './hooks/useSlideSizes';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import { animateOpening, animateClosing } from './helpers/ghostAnimation';
|
||||
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
|
||||
|
||||
import ShowTransition from '../ui/ShowTransition';
|
||||
@ -22,11 +37,16 @@ import StorySettings from './StorySettings';
|
||||
|
||||
import styles from './StoryViewer.module.scss';
|
||||
|
||||
const ANIMATION_DURATION = 350;
|
||||
|
||||
interface StateProps {
|
||||
isOpen: boolean;
|
||||
userId?: string;
|
||||
storyId?: number;
|
||||
story?: ApiTypeStory;
|
||||
origin?: StoryViewerOrigin;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
withAnimation?: boolean;
|
||||
isPrivacyModalOpen?: boolean;
|
||||
}
|
||||
|
||||
@ -34,7 +54,10 @@ function StoryViewer({
|
||||
isOpen,
|
||||
userId,
|
||||
storyId,
|
||||
story,
|
||||
origin,
|
||||
shouldSkipHistoryAnimations,
|
||||
withAnimation,
|
||||
isPrivacyModalOpen,
|
||||
}: StateProps) {
|
||||
const { closeStoryViewer, closeStoryPrivacyEditor } = getActions();
|
||||
@ -44,6 +67,14 @@ function StoryViewer({
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
|
||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(false);
|
||||
|
||||
const { bestImageData, thumbnail } = useStoryProps(story);
|
||||
const slideSizes = useSlideSizes();
|
||||
const isPrevOpen = usePrevious(isOpen);
|
||||
const prevBestImageData = usePrevious(bestImageData);
|
||||
const prevUserId = usePrevious(userId);
|
||||
const prevOrigin = usePrevious(origin);
|
||||
const isGhostAnimation = Boolean(withAnimation && !shouldSkipHistoryAnimations);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setIdStoryForDelete(undefined);
|
||||
@ -90,6 +121,29 @@ function StoryViewer({
|
||||
handleClose();
|
||||
}) : undefined), [handleClose, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isGhostAnimation && !isPrevOpen && isOpen && userId && thumbnail && origin !== undefined) {
|
||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
animateOpening(userId, origin, thumbnail, bestImageData, slideSizes.activeSlide);
|
||||
}
|
||||
if (isGhostAnimation && isPrevOpen && !isOpen && prevUserId && prevBestImageData && prevOrigin !== undefined) {
|
||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
animateClosing(prevUserId, prevOrigin, prevBestImageData);
|
||||
}
|
||||
}, [
|
||||
isGhostAnimation,
|
||||
bestImageData,
|
||||
prevBestImageData,
|
||||
isOpen,
|
||||
isPrevOpen,
|
||||
slideSizes.activeSlide,
|
||||
thumbnail,
|
||||
userId,
|
||||
prevUserId,
|
||||
origin,
|
||||
prevOrigin,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ShowTransition
|
||||
id="StoryViewer"
|
||||
@ -137,13 +191,22 @@ function StoryViewer({
|
||||
}
|
||||
|
||||
export default memo(withGlobal((global): StateProps => {
|
||||
const { shouldSkipHistoryAnimations, storyViewer: { storyId, userId, isPrivacyModalOpen } } = selectTabState(global);
|
||||
const {
|
||||
shouldSkipHistoryAnimations, storyViewer: {
|
||||
storyId, userId, isPrivacyModalOpen, origin,
|
||||
},
|
||||
} = selectTabState(global);
|
||||
const story = userId && storyId ? selectUserStory(global, userId, storyId) : undefined;
|
||||
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
|
||||
|
||||
return {
|
||||
isOpen: selectIsStoryViewerOpen(global),
|
||||
shouldSkipHistoryAnimations,
|
||||
userId,
|
||||
storyId,
|
||||
story,
|
||||
origin,
|
||||
withAnimation,
|
||||
isPrivacyModalOpen,
|
||||
};
|
||||
})(StoryViewer));
|
||||
|
||||
237
src/components/story/helpers/ghostAnimation.ts
Normal file
237
src/components/story/helpers/ghostAnimation.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import type { IDimensions } from '../../../global/types';
|
||||
import { StoryViewerOrigin } from '../../../types';
|
||||
import { getUserStoryHtmlId } from '../../../global/helpers';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import fastBlur from '../../../lib/fastBlur';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../../config';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
|
||||
import styles from '../StoryViewer.module.scss';
|
||||
import storyRibbonStyles from '../StoryRibbon.module.scss';
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
const OFFSET_DESKTOP = 3.5 * REM;
|
||||
const OFFSET_MOBILE = 4 * REM;
|
||||
const MOBILE_WIDTH = 600;
|
||||
|
||||
export function animateOpening(
|
||||
userId: string,
|
||||
origin: StoryViewerOrigin,
|
||||
thumb: string,
|
||||
bestImageData: string | undefined,
|
||||
dimensions: IDimensions,
|
||||
) {
|
||||
const { mediaEl: fromImage } = getNodes(origin, userId);
|
||||
if (!fromImage) {
|
||||
return;
|
||||
}
|
||||
const { width: windowWidth, height: windowHeight } = windowSize.get();
|
||||
let { width: toWidth, height: toHeight } = dimensions;
|
||||
|
||||
const isMobile = windowWidth <= MOBILE_WIDTH;
|
||||
|
||||
if (isMobile) {
|
||||
toWidth = windowWidth;
|
||||
toHeight = windowHeight - OFFSET_MOBILE;
|
||||
}
|
||||
|
||||
const toLeft = isMobile ? 0 : (windowWidth - toWidth) / 2;
|
||||
const toTop = isMobile ? 0 : (windowHeight - (toHeight + OFFSET_DESKTOP)) / 2;
|
||||
|
||||
const {
|
||||
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
|
||||
} = fromImage.getBoundingClientRect();
|
||||
|
||||
const fromTranslateX = (fromLeft + fromWidth / 2) - (toLeft + toWidth / 2);
|
||||
const fromTranslateY = (fromTop + fromHeight / 2) - (toTop + toHeight / 2);
|
||||
const fromScaleX = fromWidth / toWidth;
|
||||
const fromScaleY = fromHeight / toHeight;
|
||||
|
||||
requestMutation(() => {
|
||||
const ghost = createGhost(bestImageData || thumb, !bestImageData);
|
||||
applyStyles(ghost, {
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
width: `${toWidth}px`,
|
||||
height: `${toHeight}px`,
|
||||
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
|
||||
});
|
||||
|
||||
const container = document.getElementById('StoryViewer')!;
|
||||
container.appendChild(ghost);
|
||||
document.body.classList.add('ghost-animating');
|
||||
|
||||
requestMutation(() => {
|
||||
applyStyles(ghost, {
|
||||
transform: '',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
if (container.contains(ghost)) {
|
||||
container.removeChild(ghost);
|
||||
}
|
||||
document.body.classList.remove('ghost-animating');
|
||||
});
|
||||
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function animateClosing(
|
||||
userId: string,
|
||||
origin: StoryViewerOrigin,
|
||||
bestImageData: string,
|
||||
) {
|
||||
const { mediaEl: toImage } = getNodes(origin, userId);
|
||||
|
||||
const fromImage = document.getElementById('StoryViewer')!.querySelector<HTMLImageElement>(
|
||||
`.${styles.activeSlide} .${styles.media}`,
|
||||
);
|
||||
if (!fromImage || !toImage) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
|
||||
} = fromImage.getBoundingClientRect();
|
||||
const {
|
||||
top: toTop, left: toLeft, width: toWidth, height: toHeight,
|
||||
} = toImage.getBoundingClientRect();
|
||||
|
||||
const toTranslateX = (toLeft + toWidth / 2) - (fromLeft + fromWidth / 2);
|
||||
const toTranslateY = (toTop + toHeight / 2) - (fromTop + fromHeight / 2);
|
||||
const toScaleX = toWidth / fromWidth;
|
||||
const toScaleY = toHeight / fromHeight;
|
||||
|
||||
requestMutation(() => {
|
||||
const ghost = createGhost(bestImageData);
|
||||
applyStyles(ghost, {
|
||||
top: `${fromTop}px`,
|
||||
left: `${fromLeft}px`,
|
||||
width: `${fromWidth}px`,
|
||||
height: `${fromHeight}px`,
|
||||
});
|
||||
|
||||
const ghost2 = createGhost(toImage.src, undefined, true);
|
||||
const ghost2Top = (fromTop + fromHeight / 2) - fromWidth / 2;
|
||||
applyStyles(ghost2, {
|
||||
top: `${ghost2Top}px`,
|
||||
left: `${fromLeft}px`,
|
||||
width: `${fromWidth}px`,
|
||||
height: `${fromWidth}px`,
|
||||
});
|
||||
|
||||
const container = document.getElementById('StoryViewer')!;
|
||||
container.appendChild(ghost);
|
||||
document.body.appendChild(ghost2);
|
||||
document.body.classList.add('ghost-animating');
|
||||
|
||||
requestMutation(() => {
|
||||
applyStyles(ghost, {
|
||||
transform: `translate3d(${toTranslateX}px, ${toTranslateY}px, 0) scale(${toScaleX}, ${toScaleY})`,
|
||||
});
|
||||
|
||||
applyStyles(ghost2, {
|
||||
transform: `translate3d(${toTranslateX}px, ${toTranslateY}px, 0) scale(${toScaleX})`,
|
||||
opacity: '1',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
if (container.contains(ghost)) {
|
||||
container.removeChild(ghost);
|
||||
}
|
||||
|
||||
if (document.body.contains(ghost2)) {
|
||||
document.body.removeChild(ghost2);
|
||||
}
|
||||
|
||||
document.body.classList.remove('ghost-animating');
|
||||
});
|
||||
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const RADIUS = 2;
|
||||
const ITERATIONS = 2;
|
||||
|
||||
function createGhost(source: string, hasBlur = false, isGhost2 = false) {
|
||||
const ghost = document.createElement('div');
|
||||
ghost.classList.add(!isGhost2 ? styles.ghost : styles.ghost2);
|
||||
|
||||
const img = new Image();
|
||||
img.draggable = false;
|
||||
img.oncontextmenu = stopEvent;
|
||||
img.classList.add(styles.ghostImage);
|
||||
|
||||
if (hasBlur) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.classList.add(styles.thumbnail);
|
||||
img.onload = () => {
|
||||
const ctx = canvas.getContext('2d', { alpha: false })!;
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
} = img;
|
||||
|
||||
requestMutation(() => {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
if (IS_CANVAS_FILTER_SUPPORTED) {
|
||||
ctx.filter = `blur(${RADIUS}px)`;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, -RADIUS * 2, -RADIUS * 2, width + RADIUS * 4, height + RADIUS * 4);
|
||||
|
||||
if (!IS_CANVAS_FILTER_SUPPORTED) {
|
||||
fastBlur(ctx, 0, 0, width, height, RADIUS, ITERATIONS);
|
||||
}
|
||||
});
|
||||
};
|
||||
img.src = source;
|
||||
ghost.appendChild(canvas);
|
||||
} else {
|
||||
img.src = source;
|
||||
ghost.appendChild(img);
|
||||
}
|
||||
|
||||
return ghost;
|
||||
}
|
||||
|
||||
function getNodes(origin: StoryViewerOrigin, userId: string) {
|
||||
let containerSelector;
|
||||
const mediaSelector = `#${getUserStoryHtmlId(userId)}`;
|
||||
|
||||
switch (origin) {
|
||||
case StoryViewerOrigin.StoryRibbon:
|
||||
containerSelector = `#LeftColumn .${storyRibbonStyles.root}`;
|
||||
break;
|
||||
case StoryViewerOrigin.MiddleHeaderAvatar:
|
||||
containerSelector = '.MiddleHeader .Transition_slide-active .ChatInfo';
|
||||
break;
|
||||
case StoryViewerOrigin.ChatList:
|
||||
containerSelector = '#LeftColumn .chat-list';
|
||||
break;
|
||||
case StoryViewerOrigin.SearchResult:
|
||||
containerSelector = '#LeftColumn .LeftSearch';
|
||||
break;
|
||||
}
|
||||
|
||||
const container = document.querySelector<HTMLElement>(containerSelector)!;
|
||||
const mediaEls = container && container.querySelectorAll<HTMLImageElement>(`${mediaSelector} img`);
|
||||
|
||||
return {
|
||||
container,
|
||||
mediaEl: mediaEls?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function applyStyles(element: HTMLElement, css: Record<string, string>) {
|
||||
Object.assign(element.style, css);
|
||||
}
|
||||
8
src/components/story/hooks/useSlideSizes.ts
Normal file
8
src/components/story/hooks/useSlideSizes.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import useWindowSize from '../../../hooks/useWindowSize';
|
||||
import { useMemo } from '../../../lib/teact/teact';
|
||||
import { calculateSlideSizes } from '../helpers/dimensions';
|
||||
|
||||
export const useSlideSizes = () => {
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
return useMemo(() => calculateSlideSizes(windowWidth, windowHeight), [windowWidth, windowHeight]);
|
||||
};
|
||||
51
src/components/story/hooks/useStoryProps.ts
Normal file
51
src/components/story/hooks/useStoryProps.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { ApiTypeStory } from '../../../api/types';
|
||||
import { hasMessageText, getStoryMediaHash } from '../../../global/helpers';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
export const useStoryProps = (story?: ApiTypeStory) => {
|
||||
const isLoadedStory = story && 'content' in story;
|
||||
const isDeletedStory = story && 'isDeleted' in story;
|
||||
const hasText = isLoadedStory ? hasMessageText(story) : false;
|
||||
|
||||
let thumbnail: string | undefined;
|
||||
if (isLoadedStory) {
|
||||
if (story.content.photo?.thumbnail) {
|
||||
thumbnail = story.content.photo.thumbnail.dataUri;
|
||||
}
|
||||
if (story.content.video?.thumbnail?.dataUri) {
|
||||
thumbnail = story.content.video.thumbnail.dataUri;
|
||||
}
|
||||
}
|
||||
|
||||
const previewHash = isLoadedStory ? getStoryMediaHash(story) : undefined;
|
||||
const previewBlobUrl = useMedia(previewHash);
|
||||
const isVideo = Boolean(isLoadedStory && story.content.video);
|
||||
const noSound = isLoadedStory && story.content.video?.noSound;
|
||||
const fullMediaHash = isLoadedStory ? getStoryMediaHash(story, 'full') : undefined;
|
||||
const fullMediaData = useMedia(fullMediaHash, !story, isVideo ? ApiMediaFormat.Progressive : ApiMediaFormat.BlobUrl);
|
||||
const altMediaHash = isVideo && isLoadedStory ? getStoryMediaHash(story, 'full', true) : undefined;
|
||||
const altMediaData = useMedia(altMediaHash, !story, ApiMediaFormat.Progressive);
|
||||
|
||||
const hasFullData = Boolean(fullMediaData || altMediaData);
|
||||
const bestImageData = isVideo ? previewBlobUrl : fullMediaData || previewBlobUrl;
|
||||
const hasThumb = !previewBlobUrl && !hasFullData;
|
||||
|
||||
return {
|
||||
isLoadedStory,
|
||||
isDeletedStory,
|
||||
hasText,
|
||||
thumbnail,
|
||||
previewHash,
|
||||
previewBlobUrl,
|
||||
isVideo,
|
||||
noSound,
|
||||
fullMediaHash,
|
||||
fullMediaData,
|
||||
altMediaHash,
|
||||
altMediaData,
|
||||
hasFullData,
|
||||
bestImageData,
|
||||
hasThumb,
|
||||
};
|
||||
};
|
||||
@ -21,7 +21,7 @@ import * as langProvider from '../../../util/langProvider';
|
||||
|
||||
addActionHandler('openStoryViewer', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
userId, storyId, isSingleUser, isSingleStory, isPrivate, isArchive, tabId = getCurrentTabId(),
|
||||
userId, storyId, isSingleUser, isSingleStory, isPrivate, isArchive, origin, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const user = selectUser(global, userId);
|
||||
@ -52,6 +52,7 @@ addActionHandler('openStoryViewer', async (global, actions, payload): Promise<vo
|
||||
isPrivate,
|
||||
isArchive,
|
||||
isSingleStory,
|
||||
origin,
|
||||
storyIdSeenBy: undefined,
|
||||
},
|
||||
}, tabId);
|
||||
@ -60,7 +61,7 @@ addActionHandler('openStoryViewer', async (global, actions, payload): Promise<vo
|
||||
|
||||
addActionHandler('openStoryViewerByUsername', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
username, storyId, tabId = getCurrentTabId(),
|
||||
username, storyId, origin, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const chat = await fetchChatByUsername(global, username);
|
||||
@ -74,6 +75,7 @@ addActionHandler('openStoryViewerByUsername', async (global, actions, payload):
|
||||
storyId,
|
||||
isSingleUser: true,
|
||||
isSingleStory: true,
|
||||
origin,
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
@ -280,3 +280,7 @@ export function getUserColorKey(peer: ApiUser | ApiChat | undefined) {
|
||||
export function getMainUsername(userOrChat: ApiUser | ApiChat) {
|
||||
return userOrChat.usernames?.find((u) => u.isActive)?.username;
|
||||
}
|
||||
|
||||
export function getUserStoryHtmlId(userId: string) {
|
||||
return `user-story${userId}`;
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ import type {
|
||||
ShippingOption,
|
||||
ThemeKey,
|
||||
ProfileTabType,
|
||||
StoryViewerOrigin,
|
||||
} from '../types';
|
||||
import type { P2pMessage } from '../lib/secret-sauce';
|
||||
import type { ApiCredentials } from '../components/payment/PaymentModal';
|
||||
@ -361,6 +362,7 @@ export type TabState = {
|
||||
// Used for better switch animation between users.
|
||||
lastViewedByUserIds?: Record<string, number>;
|
||||
isPrivacyModalOpen?: boolean;
|
||||
origin?: StoryViewerOrigin;
|
||||
};
|
||||
|
||||
mediaViewer: {
|
||||
@ -1970,10 +1972,12 @@ export interface ActionPayloads {
|
||||
isSingleStory?: boolean;
|
||||
isPrivate?: boolean;
|
||||
isArchive?: boolean;
|
||||
origin?: StoryViewerOrigin;
|
||||
} & WithTabId;
|
||||
openStoryViewerByUsername: {
|
||||
username: string;
|
||||
storyId: number;
|
||||
origin?: StoryViewerOrigin;
|
||||
} & WithTabId;
|
||||
openPreviousStory: WithTabId | undefined;
|
||||
openNextStory: WithTabId | undefined;
|
||||
|
||||
@ -292,6 +292,13 @@ export enum MediaViewerOrigin {
|
||||
SuggestedAvatar,
|
||||
}
|
||||
|
||||
export enum StoryViewerOrigin {
|
||||
StoryRibbon,
|
||||
MiddleHeaderAvatar,
|
||||
ChatList,
|
||||
SearchResult,
|
||||
}
|
||||
|
||||
export enum AudioOrigin {
|
||||
Inline,
|
||||
SharedMedia,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user