From a5e29bd32e627ef696984186bbbba893e1422bae Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 4 Sep 2023 04:05:07 +0200 Subject: [PATCH] Stories: Follow-up (#3758) --- src/components/story/Story.tsx | 47 +++++++++++++------- src/components/story/StoryToggler.tsx | 4 ++ src/components/story/StoryViewer.module.scss | 11 ----- src/components/story/StoryViewer.tsx | 3 ++ src/components/story/StoryViewers.tsx | 19 ++++++-- src/components/ui/SearchInput.tsx | 2 +- src/global/actions/apiUpdaters/users.ts | 5 ++- src/global/reducers/stories.ts | 5 ++- 8 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 0a1b2a7af..640f78da1 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -16,7 +16,7 @@ import { } from '../../global/helpers'; import { formatRelativeTime } from '../../util/dateFormat'; import { getServerTime } from '../../util/serverTime'; -import { selectChat, selectTabState } from '../../global/selectors'; +import { selectChat, selectIsCurrentUserPremium, selectTabState } from '../../global/selectors'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout'; @@ -44,6 +44,7 @@ import MenuItem from '../ui/MenuItem'; import DropdownMenu from '../ui/DropdownMenu'; import Skeleton from '../ui/Skeleton'; import StoryCaption from './StoryCaption'; +import AvatarList from '../common/AvatarList'; import styles from './StoryViewer.module.scss'; @@ -75,13 +76,14 @@ interface StateProps { viewersExpirePeriod: number; isChatExist?: boolean; areChatSettingsLoaded?: boolean; + isCurrentUserPremium?: boolean; } const VIDEO_MIN_READY_STATE = 4; const SPACEBAR_CODE = 32; -const PRIMARY_VIDEO_MIME = 'video/mp4; codecs="hvc1"'; -const SECONDARY_VIDEO_MIME = 'video/mp4; codecs="avc1.64001E"'; +const PRIMARY_VIDEO_MIME = 'video/mp4; codecs=hvc1.1.6.L63.00'; +const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E'; function Story({ isSelf, @@ -101,6 +103,7 @@ function Story({ isChatExist, areChatSettingsLoaded, getIsAnimating, + isCurrentUserPremium, onDelete, onClose, onReport, @@ -148,7 +151,13 @@ function Story({ true, ); const areViewsExpired = Boolean( - isSelf && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(), + isSelf && !isCurrentUserPremium && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(), + ); + const canCopyLink = Boolean( + isLoadedStory + && story.isPublic + && userId !== storyChangelogUserId + && user?.usernames?.length, ); const canShare = Boolean( isLoadedStory @@ -275,11 +284,10 @@ function Story({ useEffect(() => { if (!isSelf || isDeletedStory || areViewsExpired) return; - if (story && 'recentViewerIds' in story && story.recentViewerIds?.length) return; - // Refresh recent viewers list on new stories each view + // Refresh recent viewers list each time loadStorySeenBy({ storyId }); - }, [isDeletedStory, areViewsExpired, isSelf, story, storyId]); + }, [isDeletedStory, areViewsExpired, isSelf, storyId]); useEffect(() => { if ( @@ -582,7 +590,7 @@ function Story({ onOpen={handlePauseStory} onClose={handlePlayStory} > - {lang('CopyLink')} + {canCopyLink && {lang('CopyLink')}} {canPinToProfile && ( {lang('StorySave')} )} @@ -597,11 +605,17 @@ function Story({ ); } - function renderRecentViewers() { - // No need for expensive global updates on chats and users, so we avoid them + const recentViewers = useMemo(() => { const { users: { byId: usersById } } = getGlobal(); - const { recentViewerIds, viewsCount } = story as ApiStory; + const recentViewerIds = story && 'recentViewerIds' in story ? story.recentViewerIds : undefined; + if (!recentViewerIds) return undefined; + + return recentViewerIds.map((id) => usersById[id]).filter(Boolean); + }, [story]); + + function renderRecentViewers() { + const { viewsCount } = story as ApiStory; if (!viewsCount) { return ( @@ -620,14 +634,12 @@ function Story({ )} onClick={handleOpenStorySeenBy} > - {!areViewsExpired && recentViewerIds?.map((viewerId) => ( - - ))} + )} {lang('Views', viewsCount, 'i')} @@ -780,6 +792,7 @@ export default memo(withGlobal((global, { orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds), isMuted, isSelf: currentUserId === userId, + isCurrentUserPremium: selectIsCurrentUserPremium(global), shouldForcePause, storyChangelogUserId: appConfig!.storyChangelogUserId, viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, diff --git a/src/components/story/StoryToggler.tsx b/src/components/story/StoryToggler.tsx index 3aeffdc8e..97d3f482c 100644 --- a/src/components/story/StoryToggler.tsx +++ b/src/components/story/StoryToggler.tsx @@ -42,6 +42,10 @@ function StoryToggler({ const lang = useLang(); const users = useMemo(() => { + if (orderedUserIds.length === 1) { + return [usersById[orderedUserIds[0]]]; + } + return orderedUserIds .map((id) => usersById[id]) .filter((user) => user && user.id !== currentUserId) diff --git a/src/components/story/StoryViewer.module.scss b/src/components/story/StoryViewer.module.scss index 5fc7e7711..bc3c4beed 100644 --- a/src/components/story/StoryViewer.module.scss +++ b/src/components/story/StoryViewer.module.scss @@ -646,17 +646,6 @@ } } -.recentViewer { - z-index: 3; -} -.recentViewer + .recentViewer { - margin-left: -0.5rem; - z-index: 2; -} -.recentViewer + .recentViewer + .recentViewer { - z-index: 1; -} - .recentViewersCount { margin-inline-start: 0.5rem; } diff --git a/src/components/story/StoryViewer.tsx b/src/components/story/StoryViewer.tsx index 422e62cf2..a66c739f2 100644 --- a/src/components/story/StoryViewer.tsx +++ b/src/components/story/StoryViewer.tsx @@ -10,6 +10,7 @@ import { disableDirectTextInput, enableDirectTextInput } from '../../util/direct import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useHistoryBack from '../../hooks/useHistoryBack'; +import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; import ShowTransition from '../ui/ShowTransition'; import Button from '../ui/Button'; @@ -57,8 +58,10 @@ function StoryViewer({ } disableDirectTextInput(); + const stopPriorityPlayback = dispatchPriorityPlaybackEvent(); return () => { + stopPriorityPlayback(); enableDirectTextInput(); }; }, [isOpen]); diff --git a/src/components/story/StoryViewers.tsx b/src/components/story/StoryViewers.tsx index c016a1dd1..8e74ef55c 100644 --- a/src/components/story/StoryViewers.tsx +++ b/src/components/story/StoryViewers.tsx @@ -1,7 +1,12 @@ import React, { memo, useEffect, useMemo } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import { selectStorySeenBy, selectTabState, selectUserStory } from '../../global/selectors'; +import { + selectIsCurrentUserPremium, + selectStorySeenBy, + selectTabState, + selectUserStory, +} from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatDateAtTime } from '../../util/dateFormat'; import { getServerTime } from '../../util/serverTime'; @@ -26,6 +31,7 @@ interface StateProps { viewsCount?: number; seenByDates?: Record; viewersExpirePeriod: number; + isCurrentUserPremium?: boolean; } const CLOSE_ANIMATION_DURATION = 100; @@ -35,6 +41,7 @@ function StoryViewers({ viewsCount, viewersExpirePeriod, seenByDates, + isCurrentUserPremium, }: StateProps) { const { loadStorySeenBy, openChat, closeStorySeenBy, closeStoryViewer, @@ -43,7 +50,7 @@ function StoryViewers({ const lang = useLang(); const isOpen = Boolean(storyId); - const isExpired = Boolean(storyDate) && (storyDate + viewersExpirePeriod) < getServerTime(); + const isExpired = !isCurrentUserPremium && Boolean(storyDate) && (storyDate + viewersExpirePeriod) < getServerTime(); const renderingSeenByDates = useCurrentOrPrev(seenByDates, true); const renderingIsExpired = usePrevious(isExpired) || isExpired; const renderingViewsCount = useCurrentOrPrev(viewsCount, true); @@ -58,7 +65,7 @@ function StoryViewers({ return result; }, [renderingIsExpired, renderingSeenByDates]); - const isLoading = !renderingIsExpired && (!memberIds || memberIds.length === 0); + const isLoading = !isCurrentUserPremium && !renderingIsExpired && (!memberIds || memberIds.length === 0); useEffect(() => { if (!storyId || seenByDates || renderingIsExpired) { @@ -99,6 +106,11 @@ function StoryViewers({ {renderText(lang('ExpiredViewsStub'), ['simple_markdown', 'emoji'])} )} + {isCurrentUserPremium && Boolean(!memberIds?.length) && ( +
+ {lang('ServerErrorViewers')} +
+ )} {memberIds?.map((userId) => ( { viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, storyDate, viewsCount, + isCurrentUserPremium: selectIsCurrentUserPremium(global), }; })(StoryViewers)); diff --git a/src/components/ui/SearchInput.tsx b/src/components/ui/SearchInput.tsx index 77f29d1e4..f0f7e0b39 100644 --- a/src/components/ui/SearchInput.tsx +++ b/src/components/ui/SearchInput.tsx @@ -134,7 +134,7 @@ const SearchInput: FC = ({ onKeyDown={handleKeyDown} /> { case 'updateUser': { Object.values(global.byTabId).forEach(({ id: tabId }) => { if (update.id === global.currentUserId && update.user.isPremium !== selectIsCurrentUserPremium(global)) { - // TODO Do not display modal if premium is bought from another device - if (update.user.isPremium) actions.openPremiumModal({ isSuccess: true, tabId }); + if (update.user.isPremium && global.byTabId[tabId].premiumModal) { + actions.openPremiumModal({ isSuccess: true, tabId }); + } // Reset translation cache cause premium provides additional formatting global = { diff --git a/src/global/reducers/stories.ts b/src/global/reducers/stories.ts index 57ed810a5..c0cddabc3 100644 --- a/src/global/reducers/stories.ts +++ b/src/global/reducers/stories.ts @@ -356,7 +356,10 @@ function updateOrderedStoriesUserIds(global: T, updateUse }, { active: [], archived: [] }); function sort(userId: string) { - return currentUserId === userId ? Infinity : byUserId[userId].lastUpdatedAt; + const PREMIUM_PRIORITY = 1e12; + const isPremium = selectUser(global, userId)?.isPremium; + const lastUpdated = byUserId[userId].lastUpdatedAt || 0; + return currentUserId === userId ? Infinity : (lastUpdated + (isPremium ? PREMIUM_PRIORITY : 0)); } newOrderedUserIds.archived = orderBy(