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';
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}
>
<MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem>
{canCopyLink && <MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem>}
{canPinToProfile && (
<MenuItem icon="save-story" onClick={handlePinClick}>{lang('StorySave')}</MenuItem>
)}
@ -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) => (
<Avatar
key={`viewer-${viewerId}`}
{!areViewsExpired && Boolean(recentViewers?.length) && (
<AvatarList
size="small"
peer={usersById[viewerId]}
className={styles.recentViewer}
peers={recentViewers}
/>
))}
)}
<span className={styles.recentViewersCount}>{lang('Views', viewsCount, 'i')}</span>
</div>
@ -780,6 +792,7 @@ export default memo(withGlobal<OwnProps>((global, {
orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds),
isMuted,
isSelf: currentUserId === userId,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
shouldForcePause,
storyChangelogUserId: appConfig!.storyChangelogUserId,
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,

View File

@ -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)

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 {
margin-inline-start: 0.5rem;
}

View File

@ -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]);

View File

@ -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<string, number>;
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'])}
</div>
)}
{isCurrentUserPremium && Boolean(!memberIds?.length) && (
<div className={styles.expiredText}>
{lang('ServerErrorViewers')}
</div>
)}
{memberIds?.map((userId) => (
<ListItem
key={userId}
@ -142,5 +154,6 @@ export default memo(withGlobal((global) => {
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,
storyDate,
viewsCount,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
})(StoryViewers));

View File

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

View File

@ -42,8 +42,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
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 = {

View File

@ -356,7 +356,10 @@ function updateOrderedStoriesUserIds<T extends GlobalState>(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(