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