Stories: Follow-up (#3758)

This commit is contained in:
Alexander Zinchuk 2023-09-04 04:05:07 +02:00
parent 1de3f4811e
commit a5e29bd32e
8 changed files with 61 additions and 35 deletions

View File

@ -16,7 +16,7 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import { formatRelativeTime } from '../../util/dateFormat'; import { formatRelativeTime } from '../../util/dateFormat';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
import { selectChat, selectTabState } from '../../global/selectors'; import { selectChat, selectIsCurrentUserPremium, selectTabState } from '../../global/selectors';
import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import captureKeyboardListeners from '../../util/captureKeyboardListeners';
import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout'; import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout';
@ -44,6 +44,7 @@ import MenuItem from '../ui/MenuItem';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import Skeleton from '../ui/Skeleton'; import Skeleton from '../ui/Skeleton';
import StoryCaption from './StoryCaption'; import StoryCaption from './StoryCaption';
import AvatarList from '../common/AvatarList';
import styles from './StoryViewer.module.scss'; import styles from './StoryViewer.module.scss';
@ -75,13 +76,14 @@ interface StateProps {
viewersExpirePeriod: number; viewersExpirePeriod: number;
isChatExist?: boolean; isChatExist?: boolean;
areChatSettingsLoaded?: boolean; areChatSettingsLoaded?: boolean;
isCurrentUserPremium?: boolean;
} }
const VIDEO_MIN_READY_STATE = 4; const VIDEO_MIN_READY_STATE = 4;
const SPACEBAR_CODE = 32; const SPACEBAR_CODE = 32;
const PRIMARY_VIDEO_MIME = 'video/mp4; codecs="hvc1"'; const PRIMARY_VIDEO_MIME = 'video/mp4; codecs=hvc1.1.6.L63.00';
const SECONDARY_VIDEO_MIME = 'video/mp4; codecs="avc1.64001E"'; const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E';
function Story({ function Story({
isSelf, isSelf,
@ -101,6 +103,7 @@ function Story({
isChatExist, isChatExist,
areChatSettingsLoaded, areChatSettingsLoaded,
getIsAnimating, getIsAnimating,
isCurrentUserPremium,
onDelete, onDelete,
onClose, onClose,
onReport, onReport,
@ -148,7 +151,13 @@ function Story({
true, true,
); );
const areViewsExpired = Boolean( 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( const canShare = Boolean(
isLoadedStory isLoadedStory
@ -275,11 +284,10 @@ function Story({
useEffect(() => { useEffect(() => {
if (!isSelf || isDeletedStory || areViewsExpired) return; 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 }); loadStorySeenBy({ storyId });
}, [isDeletedStory, areViewsExpired, isSelf, story, storyId]); }, [isDeletedStory, areViewsExpired, isSelf, storyId]);
useEffect(() => { useEffect(() => {
if ( if (
@ -582,7 +590,7 @@ function Story({
onOpen={handlePauseStory} onOpen={handlePauseStory}
onClose={handlePlayStory} onClose={handlePlayStory}
> >
<MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem> {canCopyLink && <MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem>}
{canPinToProfile && ( {canPinToProfile && (
<MenuItem icon="save-story" onClick={handlePinClick}>{lang('StorySave')}</MenuItem> <MenuItem icon="save-story" onClick={handlePinClick}>{lang('StorySave')}</MenuItem>
)} )}
@ -597,11 +605,17 @@ function Story({
); );
} }
function renderRecentViewers() { const recentViewers = useMemo(() => {
// No need for expensive global updates on chats and users, so we avoid them
const { users: { byId: usersById } } = getGlobal(); 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) { if (!viewsCount) {
return ( return (
@ -620,14 +634,12 @@ function Story({
)} )}
onClick={handleOpenStorySeenBy} onClick={handleOpenStorySeenBy}
> >
{!areViewsExpired && recentViewerIds?.map((viewerId) => ( {!areViewsExpired && Boolean(recentViewers?.length) && (
<Avatar <AvatarList
key={`viewer-${viewerId}`}
size="small" size="small"
peer={usersById[viewerId]} peers={recentViewers}
className={styles.recentViewer}
/> />
))} )}
<span className={styles.recentViewersCount}>{lang('Views', viewsCount, 'i')}</span> <span className={styles.recentViewersCount}>{lang('Views', viewsCount, 'i')}</span>
</div> </div>
@ -780,6 +792,7 @@ export default memo(withGlobal<OwnProps>((global, {
orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds), orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds),
isMuted, isMuted,
isSelf: currentUserId === userId, isSelf: currentUserId === userId,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
shouldForcePause, shouldForcePause,
storyChangelogUserId: appConfig!.storyChangelogUserId, storyChangelogUserId: appConfig!.storyChangelogUserId,
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,

View File

@ -42,6 +42,10 @@ function StoryToggler({
const lang = useLang(); const lang = useLang();
const users = useMemo(() => { const users = useMemo(() => {
if (orderedUserIds.length === 1) {
return [usersById[orderedUserIds[0]]];
}
return orderedUserIds return orderedUserIds
.map((id) => usersById[id]) .map((id) => usersById[id])
.filter((user) => user && user.id !== currentUserId) .filter((user) => user && user.id !== currentUserId)

View File

@ -646,17 +646,6 @@
} }
} }
.recentViewer {
z-index: 3;
}
.recentViewer + .recentViewer {
margin-left: -0.5rem;
z-index: 2;
}
.recentViewer + .recentViewer + .recentViewer {
z-index: 1;
}
.recentViewersCount { .recentViewersCount {
margin-inline-start: 0.5rem; margin-inline-start: 0.5rem;
} }

View File

@ -10,6 +10,7 @@ import { disableDirectTextInput, enableDirectTextInput } from '../../util/direct
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
import ShowTransition from '../ui/ShowTransition'; import ShowTransition from '../ui/ShowTransition';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -57,8 +58,10 @@ function StoryViewer({
} }
disableDirectTextInput(); disableDirectTextInput();
const stopPriorityPlayback = dispatchPriorityPlaybackEvent();
return () => { return () => {
stopPriorityPlayback();
enableDirectTextInput(); enableDirectTextInput();
}; };
}, [isOpen]); }, [isOpen]);

View File

@ -1,7 +1,12 @@
import React, { memo, useEffect, useMemo } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; 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 buildClassName from '../../util/buildClassName';
import { formatDateAtTime } from '../../util/dateFormat'; import { formatDateAtTime } from '../../util/dateFormat';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
@ -26,6 +31,7 @@ interface StateProps {
viewsCount?: number; viewsCount?: number;
seenByDates?: Record<string, number>; seenByDates?: Record<string, number>;
viewersExpirePeriod: number; viewersExpirePeriod: number;
isCurrentUserPremium?: boolean;
} }
const CLOSE_ANIMATION_DURATION = 100; const CLOSE_ANIMATION_DURATION = 100;
@ -35,6 +41,7 @@ function StoryViewers({
viewsCount, viewsCount,
viewersExpirePeriod, viewersExpirePeriod,
seenByDates, seenByDates,
isCurrentUserPremium,
}: StateProps) { }: StateProps) {
const { const {
loadStorySeenBy, openChat, closeStorySeenBy, closeStoryViewer, loadStorySeenBy, openChat, closeStorySeenBy, closeStoryViewer,
@ -43,7 +50,7 @@ function StoryViewers({
const lang = useLang(); const lang = useLang();
const isOpen = Boolean(storyId); 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 renderingSeenByDates = useCurrentOrPrev(seenByDates, true);
const renderingIsExpired = usePrevious(isExpired) || isExpired; const renderingIsExpired = usePrevious(isExpired) || isExpired;
const renderingViewsCount = useCurrentOrPrev(viewsCount, true); const renderingViewsCount = useCurrentOrPrev(viewsCount, true);
@ -58,7 +65,7 @@ function StoryViewers({
return result; return result;
}, [renderingIsExpired, renderingSeenByDates]); }, [renderingIsExpired, renderingSeenByDates]);
const isLoading = !renderingIsExpired && (!memberIds || memberIds.length === 0); const isLoading = !isCurrentUserPremium && !renderingIsExpired && (!memberIds || memberIds.length === 0);
useEffect(() => { useEffect(() => {
if (!storyId || seenByDates || renderingIsExpired) { if (!storyId || seenByDates || renderingIsExpired) {
@ -99,6 +106,11 @@ function StoryViewers({
{renderText(lang('ExpiredViewsStub'), ['simple_markdown', 'emoji'])} {renderText(lang('ExpiredViewsStub'), ['simple_markdown', 'emoji'])}
</div> </div>
)} )}
{isCurrentUserPremium && Boolean(!memberIds?.length) && (
<div className={styles.expiredText}>
{lang('ServerErrorViewers')}
</div>
)}
{memberIds?.map((userId) => ( {memberIds?.map((userId) => (
<ListItem <ListItem
key={userId} key={userId}
@ -142,5 +154,6 @@ export default memo(withGlobal((global) => {
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,
storyDate, storyDate,
viewsCount, viewsCount,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
}; };
})(StoryViewers)); })(StoryViewers));

View File

@ -134,7 +134,7 @@ const SearchInput: FC<OwnProps> = ({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
<Transition <Transition
name="zoomFade" name="fade"
shouldCleanup shouldCleanup
activeKey={Number(isLoading)} activeKey={Number(isLoading)}
className="icon-container" className="icon-container"

View File

@ -42,8 +42,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateUser': { case 'updateUser': {
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (update.id === global.currentUserId && update.user.isPremium !== selectIsCurrentUserPremium(global)) { 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 && global.byTabId[tabId].premiumModal) {
if (update.user.isPremium) actions.openPremiumModal({ isSuccess: true, tabId }); actions.openPremiumModal({ isSuccess: true, tabId });
}
// Reset translation cache cause premium provides additional formatting // Reset translation cache cause premium provides additional formatting
global = { global = {

View File

@ -356,7 +356,10 @@ function updateOrderedStoriesUserIds<T extends GlobalState>(global: T, updateUse
}, { active: [], archived: [] }); }, { active: [], archived: [] });
function sort(userId: string) { 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( newOrderedUserIds.archived = orderBy(