Stories: Follow-up (#3758)
This commit is contained in:
parent
1de3f4811e
commit
a5e29bd32e
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user