Profile: Support pinned stories (#4516)
This commit is contained in:
parent
cf8eaf270e
commit
4ca1398e34
@ -71,6 +71,7 @@ export interface GramJsAppConfig extends LimitsConfig {
|
||||
story_expire_period: number;
|
||||
story_viewers_expire_period: number;
|
||||
stories_changelog_user_id?: number;
|
||||
stories_pinned_to_top_count_max?: number;
|
||||
// Boosts
|
||||
group_transcribe_level_min?: number;
|
||||
new_noncontact_peers_require_premium_without_ownpremium?: boolean;
|
||||
@ -152,6 +153,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
storyExpirePeriod: appConfig.story_expire_period ?? STORY_EXPIRE_PERIOD,
|
||||
storyViewersExpirePeriod: appConfig.story_viewers_expire_period ?? STORY_VIEWERS_EXPIRE_PERIOD,
|
||||
storyChangelogUserId: appConfig.stories_changelog_user_id?.toString() ?? SERVICE_NOTIFICATIONS_USER_ID,
|
||||
maxPinnedStoriesCount: appConfig.stories_pinned_to_top_count_max,
|
||||
groupTranscribeLevelMin: appConfig.group_transcribe_level_min,
|
||||
canLimitNewMessagesWithoutPremium: appConfig.new_noncontact_peers_require_premium_without_ownpremium,
|
||||
bandwidthPremiumNotifyPeriod: appConfig.upload_premium_speedup_notify_period,
|
||||
|
||||
@ -66,7 +66,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
||||
content,
|
||||
isPublic,
|
||||
isEdited: edited,
|
||||
isPinned: pinned,
|
||||
isInProfile: pinned,
|
||||
isForContacts: contacts,
|
||||
isForSelectedContacts: selectedContacts,
|
||||
isForCloseFriends: closeFriends,
|
||||
|
||||
@ -77,15 +77,15 @@ export async function fetchAllStories({
|
||||
const allUserStories = result.peerStories.reduce<Record<string, ApiPeerStories>>((acc, peerStories) => {
|
||||
const peerId = getApiChatIdFromMtpPeer(peerStories.peer);
|
||||
const stories = buildApiPeerStories(peerStories);
|
||||
const { pinnedIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce<
|
||||
const { profileIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce<
|
||||
{
|
||||
pinnedIds: number[];
|
||||
profileIds: number[];
|
||||
orderedIds: number[];
|
||||
lastUpdatedAt?: number;
|
||||
}
|
||||
>((dataAcc, story) => {
|
||||
if ('isPinned' in story && story.isPinned) {
|
||||
dataAcc.pinnedIds.push(story.id);
|
||||
if ('isInProfile' in story && story.isInProfile) {
|
||||
dataAcc.profileIds.push(story.id);
|
||||
}
|
||||
if (!('isDeleted' in story)) {
|
||||
dataAcc.orderedIds.push(story.id);
|
||||
@ -94,7 +94,7 @@ export async function fetchAllStories({
|
||||
|
||||
return dataAcc;
|
||||
}, {
|
||||
pinnedIds: [],
|
||||
profileIds: [],
|
||||
orderedIds: [],
|
||||
lastUpdatedAt: undefined,
|
||||
});
|
||||
@ -106,7 +106,7 @@ export async function fetchAllStories({
|
||||
acc[peerId] = {
|
||||
byId: stories,
|
||||
orderedIds,
|
||||
pinnedIds,
|
||||
profileIds,
|
||||
lastUpdatedAt,
|
||||
lastReadId: peerStories.maxReadId,
|
||||
};
|
||||
@ -154,7 +154,7 @@ export async function fetchPeerStories({
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPeerPinnedStories({
|
||||
export function fetchPeerProfileStories({
|
||||
peer, offsetId,
|
||||
}: {
|
||||
peer: ApiPeer;
|
||||
@ -221,6 +221,7 @@ export async function fetchPeerStoriesByIds({ peer, ids }: { peer: ApiPeer; ids:
|
||||
return {
|
||||
chats,
|
||||
users,
|
||||
pinnedIds: result.pinnedToTop,
|
||||
stories,
|
||||
};
|
||||
}
|
||||
@ -246,11 +247,26 @@ export function deleteStory({ peer, storyId }: { peer: ApiPeer; storyId: number
|
||||
}));
|
||||
}
|
||||
|
||||
export function toggleStoryPinned({ peer, storyId, isPinned }: { peer: ApiPeer; storyId: number; isPinned?: boolean }) {
|
||||
export function toggleStoryInProfile({
|
||||
peer, storyId, isInProfile,
|
||||
}: {
|
||||
peer: ApiPeer; storyId: number; isInProfile?: boolean;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.stories.TogglePinned({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
id: [storyId],
|
||||
pinned: isPinned,
|
||||
pinned: isInProfile,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toggleStoryPinnedToTop({
|
||||
peer, storyIds,
|
||||
}: {
|
||||
peer: ApiPeer; storyIds: number[];
|
||||
}) {
|
||||
return invokeRequest(new GramJs.stories.TogglePinnedToTop({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
id: storyIds,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -422,6 +438,7 @@ async function fetchCommonStoriesRequest({ method, peerId }: {
|
||||
users,
|
||||
chats,
|
||||
stories,
|
||||
pinnedIds: result.pinnedToTop,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -203,6 +203,7 @@ export interface ApiAppConfig {
|
||||
storyExpirePeriod: number;
|
||||
storyViewersExpirePeriod: number;
|
||||
storyChangelogUserId: string;
|
||||
maxPinnedStoriesCount?: number;
|
||||
groupTranscribeLevelMin?: number;
|
||||
canLimitNewMessagesWithoutPremium?: boolean;
|
||||
bandwidthPremiumNotifyPeriod?: number;
|
||||
|
||||
@ -10,7 +10,7 @@ export interface ApiStory {
|
||||
date: number;
|
||||
expireDate: number;
|
||||
content: MediaContent;
|
||||
isPinned?: boolean;
|
||||
isInProfile?: boolean;
|
||||
isEdited?: boolean;
|
||||
isForCloseFriends?: boolean;
|
||||
isForContacts?: boolean;
|
||||
@ -56,8 +56,11 @@ export type ApiTypeStory = ApiStory | ApiStorySkipped | ApiStoryDeleted;
|
||||
export type ApiPeerStories = {
|
||||
byId: Record<number, ApiTypeStory>;
|
||||
orderedIds: number[]; // Actual peer stories
|
||||
pinnedIds: number[]; // Profile Shared Media: Pinned Stories tab
|
||||
profileIds: number[]; // Profile Shared Media: Profile Stories tab
|
||||
isFullyLoaded?: boolean;
|
||||
pinnedIds?: number[]; // Profile Shared Media: Pinned profile stories
|
||||
archiveIds?: number[]; // Profile Shared Media: Archive Stories tab
|
||||
isArchiveFullyLoaded?: boolean;
|
||||
lastUpdatedAt?: number;
|
||||
lastReadId?: number;
|
||||
};
|
||||
|
||||
@ -109,6 +109,7 @@ type StateProps = {
|
||||
adminMembersById?: Record<string, ApiChatMember>;
|
||||
commonChatIds?: string[];
|
||||
storyIds?: number[];
|
||||
pinnedStoryIds?: number[];
|
||||
archiveStoryIds?: number[];
|
||||
storyByIds?: Record<number, ApiTypeStory>;
|
||||
chatsById: Record<string, ApiChat>;
|
||||
@ -155,6 +156,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
messagesById,
|
||||
foundIds,
|
||||
storyIds,
|
||||
pinnedStoryIds,
|
||||
archiveStoryIds,
|
||||
storyByIds,
|
||||
mediaSearchType,
|
||||
@ -194,7 +196,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
focusMessage,
|
||||
loadProfilePhotos,
|
||||
setNewChatMembersDialogState,
|
||||
loadPeerPinnedStories,
|
||||
loadPeerProfileStories,
|
||||
loadStoriesArchive,
|
||||
openPremiumModal,
|
||||
loadChannelRecommendations,
|
||||
@ -264,7 +266,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
|
||||
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
|
||||
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
|
||||
loadPeerPinnedStories({ peerId: chatId, offsetId });
|
||||
loadPeerProfileStories({ peerId: chatId, offsetId });
|
||||
}, [chatId]);
|
||||
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
|
||||
loadStoriesArchive({ peerId: currentUserId!, offsetId });
|
||||
@ -287,6 +289,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
foundIds,
|
||||
threadId,
|
||||
storyIds,
|
||||
pinnedStoryIds,
|
||||
archiveStoryIds,
|
||||
similarChannels,
|
||||
);
|
||||
@ -484,11 +487,11 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
))
|
||||
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
|
||||
(viewportIds as number[])!.map((id) => storyByIds?.[id] && (
|
||||
(viewportIds as number[])!.map((id, i) => storyByIds?.[id] && (
|
||||
<MediaStory
|
||||
teactOrderKey={i}
|
||||
key={`${resultType}_${id}`}
|
||||
story={storyByIds[id]}
|
||||
isProtected={isChatProtected}
|
||||
isArchive={resultType === 'storiesArchive'}
|
||||
/>
|
||||
))
|
||||
@ -718,7 +721,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories))
|
||||
&& !isSavedDialog;
|
||||
const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined;
|
||||
const storyIds = peerStories?.pinnedIds;
|
||||
const storyIds = peerStories?.profileIds;
|
||||
const pinnedStoryIds = peerStories?.pinnedIds;
|
||||
const storyByIds = peerStories?.byId;
|
||||
const archiveStoryIds = peerStories?.archiveIds;
|
||||
|
||||
@ -743,6 +747,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
userStatusesById,
|
||||
chatsById,
|
||||
storyIds,
|
||||
pinnedStoryIds,
|
||||
archiveStoryIds,
|
||||
storyByIds,
|
||||
isChatProtected: chat?.isProtected,
|
||||
|
||||
@ -29,6 +29,7 @@ export default function useProfileViewportIds(
|
||||
foundIds?: number[],
|
||||
threadId?: ThreadId,
|
||||
storyIds?: number[],
|
||||
pinnedStoryIds?: number[],
|
||||
archiveStoryIds?: number[],
|
||||
similarChannels?: string[],
|
||||
) {
|
||||
@ -82,8 +83,20 @@ export default function useProfileViewportIds(
|
||||
loadCommonChats, chatIds,
|
||||
);
|
||||
|
||||
const sortedStoryIds = useMemo(() => {
|
||||
if (!storyIds?.length) return storyIds;
|
||||
const pinnedStoryIdsSet = new Set(pinnedStoryIds);
|
||||
return storyIds.slice().sort((a, b) => {
|
||||
const aIsPinned = pinnedStoryIdsSet.has(a);
|
||||
const bIsPinned = pinnedStoryIdsSet.has(b);
|
||||
if (aIsPinned && !bIsPinned) return -1;
|
||||
if (!aIsPinned && bIsPinned) return 1;
|
||||
return b - a;
|
||||
});
|
||||
}, [storyIds, pinnedStoryIds]);
|
||||
|
||||
const [storyViewportIds, getMoreStories, noProfileInfoForStories] = useInfiniteScrollForLoadableItems(
|
||||
loadStories, storyIds,
|
||||
loadStories, sortedStoryIds,
|
||||
);
|
||||
|
||||
const [
|
||||
|
||||
@ -25,6 +25,35 @@
|
||||
vertical-align: -0.1875rem;
|
||||
}
|
||||
|
||||
.overlayIcon {
|
||||
position: absolute;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
|
||||
filter: drop-shadow(0 0 0.25rem black);
|
||||
}
|
||||
|
||||
.pinnedIcon {
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
}
|
||||
|
||||
.viewsCount {
|
||||
bottom: 0.25rem;
|
||||
left: 0.25rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.duration {
|
||||
bottom: 0.25rem;
|
||||
right: 0.25rem;
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
position: relative;
|
||||
z-index: var(--z-right-column-menu);
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiStory, ApiTypeStory } from '../../api/types';
|
||||
|
||||
import { getStoryMediaHash } from '../../global/helpers';
|
||||
import { selectChat, selectPinnedStories } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatMediaDuration } from '../../util/date/dateFormat';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||
|
||||
@ -16,6 +18,7 @@ import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
|
||||
import Icon from '../common/Icon';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MediaAreaOverlay from './mediaArea/MediaAreaOverlay';
|
||||
@ -24,15 +27,23 @@ import styles from './MediaStory.module.scss';
|
||||
|
||||
interface OwnProps {
|
||||
story: ApiTypeStory;
|
||||
isProtected?: boolean;
|
||||
isArchive?: boolean;
|
||||
}
|
||||
|
||||
function MediaStory({ story, isProtected, isArchive }: OwnProps) {
|
||||
interface StateProps {
|
||||
isProtected?: boolean;
|
||||
isPinned?: boolean;
|
||||
canPin?: boolean;
|
||||
}
|
||||
|
||||
function MediaStory({
|
||||
story, isProtected, isArchive, isPinned, canPin,
|
||||
}: OwnProps & StateProps) {
|
||||
const {
|
||||
openStoryViewer,
|
||||
loadPeerSkippedStories,
|
||||
toggleStoryPinned,
|
||||
toggleStoryInProfile,
|
||||
toggleStoryPinnedToTop,
|
||||
showNotification,
|
||||
} = getActions();
|
||||
|
||||
@ -50,6 +61,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
|
||||
const isOwn = isFullyLoaded && story.isOut;
|
||||
const isDeleted = story && 'isDeleted' in story;
|
||||
const video = isFullyLoaded ? (story as ApiStory).content.video : undefined;
|
||||
const duration = video && formatMediaDuration(video.duration);
|
||||
const imageHash = isFullyLoaded ? getStoryMediaHash(story as ApiStory) : undefined;
|
||||
const imgBlobUrl = useMedia(imageHash);
|
||||
const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri;
|
||||
@ -90,26 +102,30 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
|
||||
handleBeforeContextMenu(e);
|
||||
});
|
||||
|
||||
const handlePinClick = useLastCallback((e: React.SyntheticEvent) => {
|
||||
const handleUnarchiveClick = useLastCallback((e: React.SyntheticEvent) => {
|
||||
stopEvent(e);
|
||||
|
||||
toggleStoryPinned({ peerId, storyId: story.id, isPinned: true });
|
||||
toggleStoryInProfile({ peerId, storyId: story.id, isInProfile: true });
|
||||
showNotification({
|
||||
message: lang('Story.ToastSavedToProfileText'),
|
||||
});
|
||||
handleContextMenuClose();
|
||||
});
|
||||
|
||||
const handleUnpinClick = useLastCallback((e: React.SyntheticEvent) => {
|
||||
const handleArchiveClick = useLastCallback((e: React.SyntheticEvent) => {
|
||||
stopEvent(e);
|
||||
|
||||
toggleStoryPinned({ peerId, storyId: story.id, isPinned: false });
|
||||
toggleStoryInProfile({ peerId, storyId: story.id, isInProfile: false });
|
||||
showNotification({
|
||||
message: lang('Story.ToastRemovedFromProfileText'),
|
||||
});
|
||||
handleContextMenuClose();
|
||||
});
|
||||
|
||||
const handleTogglePinned = useLastCallback(() => {
|
||||
toggleStoryPinnedToTop({ peerId, storyId: story.id });
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@ -120,10 +136,18 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
|
||||
>
|
||||
{isDeleted && (
|
||||
<span>
|
||||
<i className={buildClassName(styles.expiredIcon, 'icon icon-story-expired')} aria-hidden />
|
||||
<Icon className={styles.expiredIcon} name="story-expired" />
|
||||
{lang('ExpiredStory')}
|
||||
</span>
|
||||
)}
|
||||
{isPinned && <Icon className={buildClassName(styles.overlayIcon, styles.pinnedIcon)} name="pin-badge" />}
|
||||
{isFullyLoaded && Boolean(story.views?.viewsCount) && (
|
||||
<span className={buildClassName(styles.overlayIcon, styles.viewsCount)}>
|
||||
<Icon name="eye" />
|
||||
{story.views.viewsCount}
|
||||
</span>
|
||||
)}
|
||||
{duration && <span className={buildClassName(styles.overlayIcon, styles.duration)}>{duration}</span>}
|
||||
<div className={styles.wrapper}>
|
||||
{thumbUrl && (
|
||||
<img src={thumbUrl} alt="" className={styles.media} draggable={false} />
|
||||
@ -145,16 +169,45 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{isArchive && <MenuItem icon="pin" onClick={handlePinClick}>{lang('StoryList.SaveToProfile')}</MenuItem>}
|
||||
{isArchive && (
|
||||
<MenuItem icon="archive" onClick={handleUnarchiveClick}>
|
||||
{lang('StoryList.SaveToProfile')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isArchive && (
|
||||
<MenuItem icon="unpin" onClick={handleUnpinClick}>
|
||||
<MenuItem icon="archive" onClick={handleArchiveClick}>
|
||||
{lang('Story.Context.RemoveFromProfile')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isArchive && !isPinned && canPin && (
|
||||
<MenuItem icon="pin" onClick={handleTogglePinned}>
|
||||
{lang('StoryList.ItemAction.Pin')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isArchive && isPinned && (
|
||||
<MenuItem icon="unpin" onClick={handleTogglePinned}>
|
||||
{lang('StoryList.ItemAction.Unpin')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MediaStory);
|
||||
export default memo(withGlobal<OwnProps>((global, { story }): StateProps => {
|
||||
const chat = selectChat(global, story.peerId);
|
||||
const isProtected = chat?.isProtected;
|
||||
|
||||
const { maxPinnedStoriesCount } = global.appConfig || {};
|
||||
const isOwn = 'isOut' in story && story.isOut;
|
||||
const pinnedStories = selectPinnedStories(global, story.peerId);
|
||||
const isPinned = pinnedStories?.some((pinnedStory) => pinnedStory.id === story.id);
|
||||
const canPinMore = isOwn && (!maxPinnedStoriesCount || (pinnedStories?.length || 0) < maxPinnedStoriesCount);
|
||||
|
||||
return {
|
||||
isProtected,
|
||||
isPinned,
|
||||
canPin: canPinMore,
|
||||
};
|
||||
})(MediaStory));
|
||||
|
||||
@ -140,7 +140,7 @@ function Story({
|
||||
loadPeerSkippedStories,
|
||||
openForwardMenu,
|
||||
copyStoryLink,
|
||||
toggleStoryPinned,
|
||||
toggleStoryInProfile,
|
||||
openChat,
|
||||
showNotification,
|
||||
openStoryPrivacyEditor,
|
||||
@ -188,11 +188,11 @@ function Story({
|
||||
const isOut = isLoadedStory && story.isOut;
|
||||
|
||||
const canPinToProfile = useCurrentOrPrev(
|
||||
isOut ? !story.isPinned : undefined,
|
||||
isOut ? !story.isInProfile : undefined,
|
||||
true,
|
||||
);
|
||||
const canUnpinFromProfile = useCurrentOrPrev(
|
||||
isOut ? story.isPinned : undefined,
|
||||
isOut ? story.isInProfile : undefined,
|
||||
true,
|
||||
);
|
||||
const areViewsExpired = Boolean(
|
||||
@ -460,11 +460,11 @@ function Story({
|
||||
});
|
||||
|
||||
const handlePinClick = useLastCallback(() => {
|
||||
toggleStoryPinned({ peerId, storyId, isPinned: true });
|
||||
toggleStoryInProfile({ peerId, storyId, isInProfile: true });
|
||||
});
|
||||
|
||||
const handleUnpinClick = useLastCallback(() => {
|
||||
toggleStoryPinned({ peerId, storyId, isPinned: false });
|
||||
toggleStoryInProfile({ peerId, storyId, isInProfile: false });
|
||||
});
|
||||
|
||||
const handleDeleteStoryClick = useLastCallback(() => {
|
||||
|
||||
@ -91,12 +91,12 @@ function StorySettings({
|
||||
currentUserId,
|
||||
onClose,
|
||||
}: OwnProps & StateProps) {
|
||||
const { editStoryPrivacy, toggleStoryPinned } = getActions();
|
||||
const { editStoryPrivacy, toggleStoryInProfile } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [isOpenModal, openModal, closeModal] = useFlag(false);
|
||||
const [privacy, setPrivacy] = useState<ApiPrivacySettings | undefined>(visibility);
|
||||
const [isPinned, setIsPinned] = useState(story?.isPinned);
|
||||
const [isPinned, setIsPinned] = useState(story?.isInProfile);
|
||||
const [activeKey, setActiveKey] = useState<Screens>(Screens.privacy);
|
||||
const [editingBlockingCategory, setEditingBlockingCategory] = useState<PrivacyVisibility>('everybody');
|
||||
const isBackButton = activeKey !== Screens.privacy;
|
||||
@ -195,8 +195,8 @@ function StorySettings({
|
||||
storyId: story!.id,
|
||||
privacy: privacy!,
|
||||
});
|
||||
if (story!.isPinned !== isPinned) {
|
||||
toggleStoryPinned({ peerId: story!.peerId, storyId: story!.id, isPinned });
|
||||
if (story!.isInProfile !== isPinned) {
|
||||
toggleStoryInProfile({ peerId: story!.peerId, storyId: story!.id, isInProfile: isPinned });
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
@ -17,7 +17,8 @@ import {
|
||||
updateLastReadStoryForPeer,
|
||||
updateLastViewedStoryForPeer,
|
||||
updatePeer,
|
||||
updatePeerPinnedStory,
|
||||
updatePeerProfileStory,
|
||||
updatePeerStoriesFullyLoaded,
|
||||
updatePeerStoriesHidden,
|
||||
updatePeerStory,
|
||||
updatePeerStoryViews,
|
||||
@ -29,6 +30,7 @@ import {
|
||||
} from '../../reducers';
|
||||
import {
|
||||
selectPeer, selectPeerStories, selectPeerStory,
|
||||
selectPinnedStories,
|
||||
} from '../../selectors';
|
||||
|
||||
const INFINITE_LOOP_MARKER = 100;
|
||||
@ -153,7 +155,7 @@ addActionHandler('loadPeerSkippedStories', async (global, actions, payload): Pro
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = addStoriesForPeer(global, peerId, result.stories);
|
||||
global = addStoriesForPeer(global, peerId, result.stories, result.pinnedIds);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -170,7 +172,7 @@ addActionHandler('viewStory', async (global, actions, payload): Promise<void> =>
|
||||
|
||||
const serverTime = getServerTime();
|
||||
|
||||
if (story.expireDate < serverTime && story.isPinned) {
|
||||
if (story.expireDate < serverTime && story.isInProfile) {
|
||||
void callApi('viewStory', { peer, storyId });
|
||||
}
|
||||
|
||||
@ -212,8 +214,8 @@ addActionHandler('deleteStory', async (global, actions, payload): Promise<void>
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('toggleStoryPinned', async (global, actions, payload): Promise<void> => {
|
||||
const { peerId, storyId, isPinned } = payload;
|
||||
addActionHandler('toggleStoryInProfile', async (global, actions, payload): Promise<void> => {
|
||||
const { peerId, storyId, isInProfile } = payload;
|
||||
|
||||
const peer = selectPeer(global, peerId);
|
||||
if (!peer) {
|
||||
@ -221,16 +223,63 @@ addActionHandler('toggleStoryPinned', async (global, actions, payload): Promise<
|
||||
}
|
||||
|
||||
const story = selectPeerStory(global, peerId, storyId);
|
||||
const currentIsPinned = story && 'content' in story ? story.isPinned : undefined;
|
||||
global = updatePeerStory(global, peerId, storyId, { isPinned });
|
||||
global = updatePeerPinnedStory(global, peerId, storyId, isPinned);
|
||||
const currentIsPinned = story && 'content' in story ? story.isInProfile : undefined;
|
||||
global = updatePeerStory(global, peerId, storyId, { isInProfile });
|
||||
global = updatePeerProfileStory(global, peerId, storyId, isInProfile);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('toggleStoryPinned', { peer, storyId, isPinned });
|
||||
const result = await callApi('toggleStoryInProfile', { peer, storyId, isInProfile });
|
||||
if (!result?.length) {
|
||||
global = getGlobal();
|
||||
global = updatePeerStory(global, peerId, storyId, { isInProfile: currentIsPinned });
|
||||
global = updatePeerProfileStory(global, peerId, storyId, currentIsPinned);
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('toggleStoryPinnedToTop', async (global, actions, payload): Promise<void> => {
|
||||
const { peerId, storyId } = payload;
|
||||
const peer = selectPeer(global, peerId);
|
||||
const peerStories = selectPeerStories(global, peerId);
|
||||
if (!peer || !peerStories) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPinnedIds = selectPinnedStories(global, peerId)?.map((s) => s.id) || [];
|
||||
const isRemoving = oldPinnedIds.includes(storyId);
|
||||
const newPinnedIds = isRemoving ? oldPinnedIds.filter((id) => id !== storyId) : [...oldPinnedIds, storyId];
|
||||
|
||||
global = {
|
||||
...getGlobal(),
|
||||
stories: {
|
||||
...getGlobal().stories,
|
||||
byPeerId: {
|
||||
...getGlobal().stories.byPeerId,
|
||||
[peerId]: {
|
||||
...peerStories,
|
||||
pinnedIds: newPinnedIds.sort((a, b) => b - a),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
const result = await callApi('toggleStoryPinnedToTop', { peer, storyIds: newPinnedIds });
|
||||
|
||||
if (!result) {
|
||||
global = getGlobal();
|
||||
global = updatePeerStory(global, peerId, storyId, { isPinned: currentIsPinned });
|
||||
global = updatePeerPinnedStory(global, peerId, storyId, currentIsPinned);
|
||||
global = {
|
||||
...global,
|
||||
stories: {
|
||||
...global.stories,
|
||||
byPeerId: {
|
||||
...global.stories.byPeerId,
|
||||
[peerId]: {
|
||||
...peerStories,
|
||||
pinnedIds: oldPinnedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
@ -255,29 +304,35 @@ addActionHandler('loadPeerStories', async (global, actions, payload): Promise<vo
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadPeerPinnedStories', async (global, actions, payload): Promise<void> => {
|
||||
addActionHandler('loadPeerProfileStories', async (global, actions, payload): Promise<void> => {
|
||||
const { peerId, offsetId } = payload;
|
||||
const peer = selectPeer(global, peerId);
|
||||
if (!peer) {
|
||||
const peerStories = selectPeerStories(global, peerId);
|
||||
if (!peer || peerStories?.isFullyLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await callApi('fetchPeerPinnedStories', { peer, offsetId });
|
||||
const result = await callApi('fetchPeerProfileStories', { peer, offsetId });
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
if (Object.values(result.stories).length === 0) {
|
||||
global = updatePeerStoriesFullyLoaded(global, peerId, true);
|
||||
}
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = addStoriesForPeer(global, peerId, result.stories);
|
||||
global = addStoriesForPeer(global, peerId, result.stories, result.pinnedIds);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadStoriesArchive', async (global, actions, payload): Promise<void> => {
|
||||
const { peerId, offsetId } = payload;
|
||||
const peer = selectPeer(global, peerId);
|
||||
if (!peer) return;
|
||||
const peerStories = selectPeerStories(global, peerId);
|
||||
if (!peer || peerStories?.isArchiveFullyLoaded) return;
|
||||
|
||||
const result = await callApi('fetchStoriesArchive', { peer, offsetId });
|
||||
if (!result) {
|
||||
@ -285,9 +340,12 @@ addActionHandler('loadStoriesArchive', async (global, actions, payload): Promise
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
if (Object.values(result.stories).length === 0) {
|
||||
global = updatePeerStoriesFullyLoaded(global, peerId, true, true);
|
||||
}
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = addStoriesForPeer(global, peerId, result.stories, true);
|
||||
global = addStoriesForPeer(global, peerId, result.stories, undefined, true);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ function checkStoryExpiration() {
|
||||
stories.forEach((story) => {
|
||||
if (!('expireDate' in story)) return;
|
||||
if (story.expireDate > serverTime) return;
|
||||
if ('isPinned' in story && story.isPinned) return;
|
||||
if ('isInProfile' in story && story.isInProfile) return;
|
||||
if ('isPublic' in story && !story.isPublic) return;
|
||||
|
||||
global = removePeerStory(global, story.peerId, story.id);
|
||||
|
||||
@ -29,7 +29,7 @@ export function addStories<T extends GlobalState>(global: T, newStoriesByPeerId:
|
||||
} else {
|
||||
acc[peerId].byId = { ...acc[peerId].byId, ...newPeerStories.byId };
|
||||
acc[peerId].orderedIds = unique(newPeerStories.orderedIds.concat(acc[peerId].orderedIds));
|
||||
acc[peerId].pinnedIds = unique(newPeerStories.pinnedIds.concat(acc[peerId].pinnedIds)).sort((a, b) => b - a);
|
||||
acc[peerId].profileIds = unique(newPeerStories.profileIds.concat(acc[peerId].profileIds)).sort((a, b) => b - a);
|
||||
acc[peerId].lastUpdatedAt = newPeerStories.lastUpdatedAt;
|
||||
acc[peerId].lastReadId = newPeerStories.lastReadId;
|
||||
}
|
||||
@ -52,18 +52,19 @@ export function addStoriesForPeer<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
newStories: Record<number, ApiTypeStory>,
|
||||
newPinnedIds?: number[],
|
||||
addToArchive?: boolean,
|
||||
): T {
|
||||
const {
|
||||
byId, orderedIds, pinnedIds, archiveIds,
|
||||
byId, orderedIds, profileIds, archiveIds, pinnedIds,
|
||||
} = global.stories.byPeerId[peerId] || {};
|
||||
const deletedIds = Object.keys(newStories).filter((id) => 'isDeleted' in newStories[Number(id)]).map(Number);
|
||||
const updatedById = { ...byId, ...newStories };
|
||||
let updatedOrderedIds = [...(orderedIds || [])];
|
||||
let updatedArchiveIds = [...(archiveIds || [])];
|
||||
const updatedPinnedIds = unique(
|
||||
[...(pinnedIds || [])].concat(Object.values(newStories).reduce((ids, story) => {
|
||||
if ('isPinned' in story && story.isPinned) {
|
||||
const updatedProfileIds = unique(
|
||||
[...(profileIds || [])].concat(Object.values(newStories).reduce((ids, story) => {
|
||||
if ('isInProfile' in story && story.isInProfile) {
|
||||
ids.push(story.id);
|
||||
}
|
||||
|
||||
@ -95,7 +96,8 @@ export function addStoriesForPeer<T extends GlobalState>(
|
||||
...global.stories.byPeerId[peerId],
|
||||
byId: updatedById,
|
||||
orderedIds: updatedOrderedIds,
|
||||
pinnedIds: updatedPinnedIds,
|
||||
profileIds: updatedProfileIds,
|
||||
pinnedIds: pinnedIds || newPinnedIds,
|
||||
...(addToArchive && { archiveIds: updatedArchiveIds }),
|
||||
},
|
||||
},
|
||||
@ -129,6 +131,27 @@ export function updateStoriesForPeer<T extends GlobalState>(
|
||||
};
|
||||
}
|
||||
|
||||
export function updatePeerStoriesFullyLoaded<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
isFullyLoaded: boolean,
|
||||
isArchive?: boolean,
|
||||
): T {
|
||||
return {
|
||||
...global,
|
||||
stories: {
|
||||
...global.stories,
|
||||
byPeerId: {
|
||||
...global.stories.byPeerId,
|
||||
[peerId]: {
|
||||
...global.stories.byPeerId[peerId],
|
||||
[isArchive ? 'isArchiveFullyLoaded' : 'isFullyLoaded']: isFullyLoaded,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLastReadStoryForPeer<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
@ -258,11 +281,11 @@ export function removePeerStory<T extends GlobalState>(
|
||||
storyId: number,
|
||||
): T {
|
||||
const {
|
||||
orderedIds, pinnedIds, lastReadId, byId,
|
||||
} = selectPeerStories(global, peerId) || { orderedIds: [] as number[], pinnedIds: [] as number[] };
|
||||
orderedIds, profileIds, lastReadId, byId,
|
||||
} = selectPeerStories(global, peerId) || { orderedIds: [] as number[], profileIds: [] as number[] };
|
||||
|
||||
const newOrderedIds = orderedIds.filter((id) => id !== storyId);
|
||||
const newPinnedIds = pinnedIds.filter((id) => id !== storyId);
|
||||
const newProfileIds = profileIds.filter((id) => id !== storyId);
|
||||
const lastStoryId = newOrderedIds.length ? orderedIds[orderedIds.length - 1] : undefined;
|
||||
|
||||
const previousStoryId = orderedIds[orderedIds.indexOf(storyId) - 1];
|
||||
@ -283,7 +306,7 @@ export function removePeerStory<T extends GlobalState>(
|
||||
global = updateStoriesForPeer(global, peerId, {
|
||||
byId: newById,
|
||||
orderedIds: newOrderedIds,
|
||||
pinnedIds: newPinnedIds,
|
||||
profileIds: newProfileIds,
|
||||
lastUpdatedAt,
|
||||
lastReadId: newLastReadId,
|
||||
});
|
||||
@ -346,7 +369,7 @@ export function updatePeerStory<T extends GlobalState>(
|
||||
storyUpdate: Partial<ApiStory>,
|
||||
): T {
|
||||
const peerStories = selectPeerStories(global, peerId) || {
|
||||
byId: {}, orderedIds: [], pinnedIds: [], archiveIds: [],
|
||||
byId: {}, orderedIds: [], profileIds: [], archiveIds: [],
|
||||
};
|
||||
|
||||
return {
|
||||
@ -389,19 +412,19 @@ export function updatePeerStoryViews<T extends GlobalState>(
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePeerPinnedStory<T extends GlobalState>(
|
||||
export function updatePeerProfileStory<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
storyId: number,
|
||||
isPinned?: boolean,
|
||||
isInProfile?: boolean,
|
||||
): T {
|
||||
const peerStories = selectPeerStories(global, peerId) || {
|
||||
byId: {}, orderedIds: [], pinnedIds: [], archiveIds: [],
|
||||
byId: {}, orderedIds: [], profileIds: [], archiveIds: [],
|
||||
};
|
||||
|
||||
const newPinnedIds = isPinned
|
||||
? unique(peerStories.pinnedIds.concat(storyId)).sort((a, b) => b - a)
|
||||
: peerStories.pinnedIds.filter((id) => storyId !== id);
|
||||
const newProfileIds = isInProfile
|
||||
? unique(peerStories.profileIds.concat(storyId)).sort((a, b) => b - a)
|
||||
: peerStories.profileIds.filter((id) => storyId !== id);
|
||||
|
||||
return {
|
||||
...global,
|
||||
@ -411,7 +434,7 @@ export function updatePeerPinnedStory<T extends GlobalState>(
|
||||
...global.stories.byPeerId,
|
||||
[peerId]: {
|
||||
...peerStories,
|
||||
pinnedIds: newPinnedIds,
|
||||
profileIds: newProfileIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -35,6 +35,16 @@ export function selectPeerStory<T extends GlobalState>(
|
||||
return selectPeerStories(global, peerId)?.byId[storyId];
|
||||
}
|
||||
|
||||
export function selectPinnedStories<T extends GlobalState>(
|
||||
global: T, peerId: string,
|
||||
) {
|
||||
const stories = selectPeerStories(global, peerId);
|
||||
if (!stories?.pinnedIds?.length) return undefined;
|
||||
return stories.pinnedIds.map((id) => stories.byId[id]).filter((s) => (
|
||||
s && 'isInProfile' in s && s.isInProfile
|
||||
));
|
||||
}
|
||||
|
||||
export function selectPeerFirstUnreadStoryId<T extends GlobalState>(
|
||||
global: T, peerId: string,
|
||||
) {
|
||||
@ -124,7 +134,7 @@ function getPeerStoryIdsForViewer<T extends GlobalState>(
|
||||
isPrivate?: boolean,
|
||||
): number[] | undefined {
|
||||
const peerStories = selectPeerStories(global, peerId);
|
||||
const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'pinnedIds' : 'orderedIds';
|
||||
const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'profileIds' : 'orderedIds';
|
||||
const storyIds = peerStories?.[storySourceProp];
|
||||
|
||||
if (!peerStories || !storyIds?.length) {
|
||||
|
||||
@ -2267,7 +2267,7 @@ export interface ActionPayloads {
|
||||
loadPeerStories: {
|
||||
peerId: string;
|
||||
};
|
||||
loadPeerPinnedStories: {
|
||||
loadPeerProfileStories: {
|
||||
peerId: string;
|
||||
offsetId?: number;
|
||||
} & WithTabId;
|
||||
@ -2290,11 +2290,15 @@ export interface ActionPayloads {
|
||||
peerId: string;
|
||||
storyId: number;
|
||||
} & WithTabId;
|
||||
toggleStoryPinned: {
|
||||
toggleStoryInProfile: {
|
||||
peerId: string;
|
||||
storyId: number;
|
||||
isPinned?: boolean;
|
||||
} & WithTabId;
|
||||
isInProfile?: boolean;
|
||||
};
|
||||
toggleStoryPinnedToTop: {
|
||||
peerId: string;
|
||||
storyId: number;
|
||||
};
|
||||
toggleStoryRibbon: {
|
||||
isShown: boolean;
|
||||
isArchived?: boolean;
|
||||
|
||||
@ -1634,6 +1634,7 @@ stories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer
|
||||
stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories;
|
||||
stories.getPeerMaxIDs#535983c3 id:Vector<InputPeer> = Vector<int>;
|
||||
stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;
|
||||
stories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector<int> = Bool;
|
||||
premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList;
|
||||
premium.getMyBoosts#be77b4a = premium.MyBoosts;
|
||||
premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;
|
||||
|
||||
@ -340,6 +340,7 @@
|
||||
"stories.togglePeerStoriesHidden",
|
||||
"stories.getPeerStories",
|
||||
"stories.getStoriesViews",
|
||||
"stories.togglePinnedToTop",
|
||||
"premium.getBoostsStatus",
|
||||
"premium.getBoostersList",
|
||||
"premium.applyBoost",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user