Introduce Channel Stories (#3888)

This commit is contained in:
Alexander Zinchuk 2023-10-10 13:35:16 +02:00
parent 0d843112fa
commit 8fc3df855d
146 changed files with 2568 additions and 1566 deletions

View File

@ -123,6 +123,10 @@ type Undefined<T> = {
}; };
type OptionalCombine<A, B> = (A & B) | (A & Undefined<B>); type OptionalCombine<A, B> = (A & B) | (A & Undefined<B>);
type CommonProperties<T, U> = {
[K in keyof T & keyof U]: T[K] & U[K];
};
// Fix to make Boolean() work as !! // Fix to make Boolean() work as !!
// https://github.com/microsoft/TypeScript/issues/16655 // https://github.com/microsoft/TypeScript/issues/16655
type Falsy = false | 0 | '' | null | undefined; type Falsy = false | 0 | '' | null | undefined;

View File

@ -51,6 +51,9 @@ function buildApiChatFieldsFromPeerEntity(
const isJoinRequest = Boolean('joinRequest' in peerEntity && peerEntity.joinRequest); const isJoinRequest = Boolean('joinRequest' in peerEntity && peerEntity.joinRequest);
const usernames = buildApiUsernames(peerEntity); const usernames = buildApiUsernames(peerEntity);
const isForum = Boolean('forum' in peerEntity && peerEntity.forum); const isForum = Boolean('forum' in peerEntity && peerEntity.forum);
const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden);
const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined;
const storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable);
return { return {
isMin, isMin,
@ -77,6 +80,9 @@ function buildApiChatFieldsFromPeerEntity(
isJoinToSend, isJoinToSend,
isJoinRequest, isJoinRequest,
isForum, isForum,
areStoriesHidden,
maxStoryId,
hasStories: Boolean(maxStoryId) && !storiesUnavailable,
}; };
} }

View File

@ -471,9 +471,9 @@ export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessag
return undefined; return undefined;
} }
const userId = buildApiPeerId(media.userId, 'user'); const peerId = getApiChatIdFromMtpPeer(media.peer);
return { id: media.id, userId, ...(media.viaMention && { isMention: true }) }; return { id: media.id, peerId, ...(media.viaMention && { isMention: true }) };
} }
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll { export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
@ -564,14 +564,14 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
const attributeStory = attributes const attributeStory = attributes
?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); ?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
if (attributeStory) { if (attributeStory) {
const userId = buildApiPeerId(attributeStory.userId, 'user'); const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = { story = {
id: attributeStory.id, id: attributeStory.id,
userId, peerId,
}; };
if (attributeStory.story instanceof GramJs.StoryItem) { if (attributeStory.story instanceof GramJs.StoryItem) {
addStoryToLocalDb(attributeStory.story, userId); addStoryToLocalDb(attributeStory.story, peerId);
} }
} }

View File

@ -11,6 +11,7 @@ import type {
ApiMessageEntity, ApiMessageEntity,
ApiMessageForwardInfo, ApiMessageForwardInfo,
ApiNewPoll, ApiNewPoll,
ApiPeer,
ApiPhoto, ApiPhoto,
ApiReplyKeyboard, ApiReplyKeyboard,
ApiSponsoredMessage, ApiSponsoredMessage,
@ -19,7 +20,6 @@ import type {
ApiStorySkipped, ApiStorySkipped,
ApiThreadInfo, ApiThreadInfo,
ApiTypeReplyTo, ApiTypeReplyTo,
ApiUser,
ApiVideo, ApiVideo,
PhoneCallAction, PhoneCallAction,
} from '../../types'; } from '../../types';
@ -690,7 +690,7 @@ export function buildLocalMessage(
contact?: ApiContact, contact?: ApiContact,
groupedId?: string, groupedId?: string,
scheduledAt?: number, scheduledAt?: number,
sendAs?: ApiChat | ApiUser, sendAs?: ApiPeer,
story?: ApiStory | ApiStorySkipped, story?: ApiStory | ApiStorySkipped,
): ApiMessage { ): ApiMessage {
const localId = getNextLocalMessageId(chat.lastMessage?.id); const localId = getNextLocalMessageId(chat.lastMessage?.id);

View File

@ -146,7 +146,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI
invoice, description: text, title, photo, invoice, description: text, title, photo,
} = form; } = form;
const { const {
test, currency, prices, recurring, recurringTermsUrl, maxTipAmount, suggestedTipAmounts, test, currency, prices, recurring, termsUrl, maxTipAmount, suggestedTipAmounts,
} = invoice; } = invoice;
const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0); const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0);
@ -159,7 +159,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI
currency, currency,
isTest: test, isTest: test,
isRecurring: recurring, isRecurring: recurring,
recurringTermsUrl, termsUrl,
maxTipAmount: maxTipAmount?.toJSNumber(), maxTipAmount: maxTipAmount?.toJSNumber(),
...(suggestedTipAmounts && { suggestedTipAmounts: suggestedTipAmounts.map((tip) => tip.toJSNumber()) }), ...(suggestedTipAmounts && { suggestedTipAmounts: suggestedTipAmounts.map((tip) => tip.toJSNumber()) }),
}; };

View File

@ -35,7 +35,7 @@ function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) {
return 0; return 0;
} }
function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined { export function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined {
const { chosenOrder, count, reaction } = reactionCount; const { chosenOrder, count, reaction } = reactionCount;
const apiReaction = buildApiReaction(reaction); const apiReaction = buildApiReaction(reaction);

View File

@ -7,14 +7,14 @@ import type {
import { buildCollectionByCallback } from '../../../util/iteratees'; import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildPrivacyRules } from './common'; import { buildPrivacyRules } from './common';
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiPeerId } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildApiReaction } from './reactions'; import { buildApiReaction, buildReactionCount } from './reactions';
export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiTypeStory { export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiTypeStory {
if (story instanceof GramJs.StoryItemDeleted) { if (story instanceof GramJs.StoryItemDeleted) {
return { return {
id: story.id, id: story.id,
userId, peerId,
isDeleted: true, isDeleted: true,
}; };
} }
@ -26,7 +26,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
return { return {
id, id,
userId, peerId,
...(closeFriends && { isForCloseFriends: true }), ...(closeFriends && { isForCloseFriends: true }),
date, date,
expireDate, expireDate,
@ -37,7 +37,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
edited, pinned, expireDate, id, date, caption, edited, pinned, expireDate, id, date, caption,
entities, media, privacy, views, entities, media, privacy, views,
public: isPublic, noforwards, closeFriends, contacts, selectedContacts, public: isPublic, noforwards, closeFriends, contacts, selectedContacts,
mediaAreas, sentReaction, mediaAreas, sentReaction, out,
} = story; } = story;
const content: ApiMessage['content'] = { const content: ApiMessage['content'] = {
@ -50,7 +50,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
return { return {
id, id,
userId, peerId,
date, date,
expireDate, expireDate,
content, content,
@ -63,9 +63,11 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
...(noforwards && { noForwards: true }), ...(noforwards && { noForwards: true }),
...(views?.viewsCount && { viewsCount: views.viewsCount }), ...(views?.viewsCount && { viewsCount: views.viewsCount }),
...(views?.reactionsCount && { reactionsCount: views.reactionsCount }), ...(views?.reactionsCount && { reactionsCount: views.reactionsCount }),
...(views?.reactions && { reactions: views.reactions.map(buildReactionCount) }),
...(views?.recentViewers && { ...(views?.recentViewers && {
recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')), recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')),
}), }),
...(out && { isOut: true }),
...(privacy && { visibility: buildPrivacyRules(privacy) }), ...(privacy && { visibility: buildPrivacyRules(privacy) }),
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }), ...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }), ...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }),
@ -134,11 +136,28 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un
}; };
} }
if (area instanceof GramJs.MediaAreaSuggestedReaction) {
const {
coordinates, reaction, dark, flipped,
} = area;
const apiReaction = buildApiReaction(reaction);
if (!apiReaction) return undefined;
return {
type: 'suggestedReaction',
coordinates: buildApiMediaAreaCoordinates(coordinates),
reaction: apiReaction,
...(dark && { isDark: true }),
...(flipped && { isFlipped: true }),
};
}
return undefined; return undefined;
} }
export function buildApiUsersStories(userStories: GramJs.UserStories) { export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
const userId = buildApiPeerId(userStories.userId, 'user'); const peerId = getApiChatIdFromMtpPeer(peerStories.peer);
return buildCollectionByCallback(userStories.stories, (story) => [story.id, buildApiStory(userId, story)]); return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
} }

View File

@ -278,9 +278,9 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
} }
export function buildInputStory(story: ApiStory | ApiStorySkipped) { export function buildInputStory(story: ApiStory | ApiStorySkipped) {
const user = localDb.users[story.userId]; const peer = buildInputPeerFromLocalDb(story.peerId)!;
return new GramJs.InputMediaStory({ return new GramJs.InputMediaStory({
userId: new GramJs.InputUser({ userId: BigInt(user!.id), accessHash: user!.accessHash! }), peer,
id: story.id, id: story.id,
}); });
} }

View File

@ -81,14 +81,14 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ
} }
} }
export function addStoryToLocalDb(story: GramJs.TypeStoryItem, userId: string) { export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) {
if (!(story instanceof GramJs.StoryItem)) { if (!(story instanceof GramJs.StoryItem)) {
return; return;
} }
const storyData = { const storyData = {
id: story.id, id: story.id,
userId, peerId,
}; };
if (story.media instanceof GramJs.MessageMediaPhoto) { if (story.media instanceof GramJs.MessageMediaPhoto) {

View File

@ -12,7 +12,7 @@ const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
export type StoryRepairInfo = { export type StoryRepairInfo = {
storyData?: { storyData?: {
userId: string; peerId: string;
id: number; id: number;
}; };
}; };

View File

@ -2,7 +2,7 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiChat, ApiPhoto, ApiReportReason, ApiUser, ApiPeer, ApiPhoto, ApiReportReason,
} from '../../types'; } from '../../types';
import { buildInputPeer, buildInputPhoto, buildInputReportReason } from '../gramjsBuilders'; import { buildInputPeer, buildInputPhoto, buildInputReportReason } from '../gramjsBuilders';
@ -13,7 +13,7 @@ export async function reportPeer({
reason, reason,
description, description,
}: { }: {
peer: ApiChat | ApiUser; reason: ApiReportReason; description?: string; peer: ApiPeer; reason: ApiReportReason; description?: string;
}) { }) {
const result = await invokeRequest(new GramJs.account.ReportPeer({ const result = await invokeRequest(new GramJs.account.ReportPeer({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
@ -30,7 +30,7 @@ export async function reportProfilePhoto({
reason, reason,
description, description,
}: { }: {
peer: ApiChat | ApiUser; photo: ApiPhoto; reason: ApiReportReason; description?: string; peer: ApiPeer; photo: ApiPhoto; reason: ApiReportReason; description?: string;
}) { }) {
const photoId = buildInputPhoto(photo); const photoId = buildInputPhoto(photo);
if (!photoId) return undefined; if (!photoId) return undefined;

View File

@ -3,7 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBotApp, ApiBotApp,
ApiChat, ApiThemeParameters, ApiUser, OnApiUpdate, ApiChat, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate,
} from '../../types'; } from '../../types';
import { WEB_APP_PLATFORM } from '../../../config'; import { WEB_APP_PLATFORM } from '../../../config';
@ -131,7 +131,7 @@ export async function sendInlineBotResult({
resultId: string; resultId: string;
queryId: string; queryId: string;
replyingTo?: number; replyingTo?: number;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
isSilent?: boolean; isSilent?: boolean;
scheduleDate?: number; scheduleDate?: number;
}) { }) {
@ -180,14 +180,14 @@ export async function requestWebView({
isFromBotMenu, isFromBotMenu,
}: { }: {
isSilent?: boolean; isSilent?: boolean;
peer: ApiChat | ApiUser; peer: ApiPeer;
bot: ApiUser; bot: ApiUser;
url?: string; url?: string;
startParam?: string; startParam?: string;
replyToMessageId?: number; replyToMessageId?: number;
threadId?: number; threadId?: number;
theme?: ApiThemeParameters; theme?: ApiThemeParameters;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
isFromBotMenu?: boolean; isFromBotMenu?: boolean;
}) { }) {
const result = await invokeRequest(new GramJs.messages.RequestWebView({ const result = await invokeRequest(new GramJs.messages.RequestWebView({
@ -269,7 +269,7 @@ export async function requestAppWebView({
theme, theme,
isWriteAllowed, isWriteAllowed,
}: { }: {
peer: ApiChat | ApiUser; peer: ApiPeer;
app: ApiBotApp; app: ApiBotApp;
startParam?: string; startParam?: string;
theme?: ApiThemeParameters; theme?: ApiThemeParameters;
@ -297,12 +297,12 @@ export function prolongWebView({
sendAs, sendAs,
}: { }: {
isSilent?: boolean; isSilent?: boolean;
peer: ApiChat | ApiUser; peer: ApiPeer;
bot: ApiUser; bot: ApiUser;
queryId: string; queryId: string;
replyToMessageId?: number; replyToMessageId?: number;
threadId?: number; threadId?: number;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
}) { }) {
return invokeRequest(new GramJs.messages.ProlongWebView({ return invokeRequest(new GramJs.messages.ProlongWebView({
silent: isSilent || undefined, silent: isSilent || undefined,

View File

@ -12,6 +12,7 @@ import type {
ApiGroupCall, ApiGroupCall,
ApiMessage, ApiMessage,
ApiMessageEntity, ApiMessageEntity,
ApiPeer,
ApiPhoto, ApiPhoto,
ApiTopic, ApiTopic,
ApiUser, ApiUser,
@ -495,6 +496,7 @@ async function getFullChannelInfo(
chatPhoto, chatPhoto,
participantsHidden, participantsHidden,
translationsDisabled, translationsDisabled,
storiesPinnedAvailable,
} = result.fullChat; } = result.fullChat;
if (chatPhoto instanceof GramJs.Photo) { if (chatPhoto instanceof GramJs.Photo) {
@ -569,6 +571,7 @@ async function getFullChannelInfo(
stickerSet: stickerset ? buildStickerSet(stickerset) : undefined, stickerSet: stickerset ? buildStickerSet(stickerset) : undefined,
areParticipantsHidden: participantsHidden, areParticipantsHidden: participantsHidden,
isTranslationDisabled: translationsDisabled, isTranslationDisabled: translationsDisabled,
hasPinnedStories: Boolean(storiesPinnedAvailable),
}, },
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
userStatusesById: statusesById, userStatusesById: statusesById,
@ -1487,7 +1490,7 @@ export async function createTopic({
title: string; title: string;
iconColor?: number; iconColor?: number;
iconEmojiId?: string; iconEmojiId?: string;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
}) { }) {
const { id, accessHash } = chat; const { id, accessHash } = chat;
@ -1746,7 +1749,7 @@ export async function createChalistInvite({
}: { }: {
folderId: number; folderId: number;
title?: string; title?: string;
peers: (ApiChat | ApiUser)[]; peers: ApiPeer[];
}) { }) {
const result = await invokeRequest(new GramJs.chatlists.ExportChatlistInvite({ const result = await invokeRequest(new GramJs.chatlists.ExportChatlistInvite({
chatlist: new GramJs.InputChatlistDialogFilter({ chatlist: new GramJs.InputChatlistDialogFilter({
@ -1786,7 +1789,7 @@ export async function editChatlistInvite({
folderId: number; folderId: number;
slug: string; slug: string;
title?: string; title?: string;
peers: (ApiChat | ApiUser)[]; peers: ApiPeer[];
}) { }) {
const result = await invokeRequest(new GramJs.chatlists.EditExportedInvite({ const result = await invokeRequest(new GramJs.chatlists.EditExportedInvite({
chatlist: new GramJs.InputChatlistDialogFilter({ chatlist: new GramJs.InputChatlistDialogFilter({

View File

@ -22,6 +22,7 @@ import { pause } from '../../../util/schedulers';
import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiPeerId } from '../apiBuilders/peers';
import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users'; import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users';
import { buildInputPeerFromLocalDb } from '../gramjsBuilders';
import { import {
addEntitiesToLocalDb, addEntitiesToLocalDb,
addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log, addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log,
@ -444,17 +445,17 @@ export async function repairFileReference({
if (mediaMatchType === 'document' || mediaMatchType === 'photo') { if (mediaMatchType === 'document' || mediaMatchType === 'photo') {
const entity = mediaMatchType === 'document' ? localDb.documents[entityId] : localDb.photos[entityId]; const entity = mediaMatchType === 'document' ? localDb.documents[entityId] : localDb.photos[entityId];
if (!entity.storyData) return false; if (!entity.storyData) return false;
const user = localDb.users[entity.storyData.userId]; const peer = buildInputPeerFromLocalDb(entity.storyData.peerId);
if (!user?.accessHash) return false; if (!peer) return false;
const result = await invokeRequest(new GramJs.stories.GetStoriesByID({ const result = await invokeRequest(new GramJs.stories.GetStoriesByID({
userId: new GramJs.InputUser({ userId: user.id, accessHash: user.accessHash }), peer,
id: [entity.storyData.id], id: [entity.storyData.id],
})); }));
if (!result) return false; if (!result) return false;
addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.users);
result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.userId)); result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.peerId));
return true; return true;
} }

View File

@ -11,6 +11,7 @@ import type {
ApiMessageSearchType, ApiMessageSearchType,
ApiNewPoll, ApiNewPoll,
ApiOnProgress, ApiOnProgress,
ApiPeer,
ApiPoll, ApiPoll,
ApiReportReason, ApiReportReason,
ApiSendMessageAction, ApiSendMessageAction,
@ -18,7 +19,6 @@ import type {
ApiStory, ApiStory,
ApiStorySkipped, ApiStorySkipped,
ApiTypeReplyTo, ApiTypeReplyTo,
ApiUser,
ApiVideo, ApiVideo,
OnApiUpdate, OnApiUpdate,
} from '../../types'; } from '../../types';
@ -267,7 +267,7 @@ export function sendMessage(
scheduledAt?: number; scheduledAt?: number;
groupedId?: string; groupedId?: string;
noWebPage?: boolean; noWebPage?: boolean;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
shouldUpdateStickerSetOrder?: boolean; shouldUpdateStickerSetOrder?: boolean;
}, },
onProgress?: ApiOnProgress, onProgress?: ApiOnProgress,
@ -417,7 +417,7 @@ function sendGroupedMedia(
groupedId: string; groupedId: string;
isSilent?: boolean; isSilent?: boolean;
scheduledAt?: number; scheduledAt?: number;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
}, },
randomId: GramJs.long, randomId: GramJs.long,
localMessage: ApiMessage, localMessage: ApiMessage,
@ -785,7 +785,7 @@ export async function deleteHistory({
export async function reportMessages({ export async function reportMessages({
peer, messageIds, reason, description, peer, messageIds, reason, description,
}: { }: {
peer: ApiChat | ApiUser; messageIds: number[]; reason: ApiReportReason; description?: string; peer: ApiPeer; messageIds: number[]; reason: ApiReportReason; description?: string;
}) { }) {
const result = await invokeRequest(new GramJs.messages.Report({ const result = await invokeRequest(new GramJs.messages.Report({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
@ -800,7 +800,7 @@ export async function reportMessages({
export async function sendMessageAction({ export async function sendMessageAction({
peer, threadId, action, peer, threadId, action,
}: { }: {
peer: ApiChat | ApiUser; threadId?: number; action: ApiSendMessageAction; peer: ApiPeer; threadId?: number; action: ApiSendMessageAction;
}) { }) {
const gramAction = buildSendMessageAction(action); const gramAction = buildSendMessageAction(action);
if (!gramAction) { if (!gramAction) {
@ -1299,7 +1299,7 @@ export async function forwardMessages({
messages: ApiMessage[]; messages: ApiMessage[];
isSilent?: boolean; isSilent?: boolean;
scheduledAt?: number; scheduledAt?: number;
sendAs?: ApiUser | ApiChat; sendAs?: ApiPeer;
withMyScore?: boolean; withMyScore?: boolean;
noAuthors?: boolean; noAuthors?: boolean;
noCaptions?: boolean; noCaptions?: boolean;
@ -1517,7 +1517,7 @@ export async function fetchSendAs({
export function saveDefaultSendAs({ export function saveDefaultSendAs({
sendAs, chat, sendAs, chat,
}: { }: {
sendAs: ApiChat | ApiUser; chat: ApiChat; sendAs: ApiPeer; chat: ApiChat;
}) { }) {
return invokeRequest(new GramJs.messages.SaveDefaultSendAs({ return invokeRequest(new GramJs.messages.SaveDefaultSendAs({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),

View File

@ -2,22 +2,29 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiInputPrivacyRules } from '../../../types'; import type { ApiInputPrivacyRules } from '../../../types';
import type { import type {
ApiReaction, ApiReportReason, ApiStealthMode, ApiChat,
ApiTypeStory, ApiUser, ApiUserStories, ApiPeer,
ApiPeerStories,
ApiReaction,
ApiReportReason,
ApiStealthMode,
ApiTypeStory,
ApiUser,
} from '../../types'; } from '../../types';
import { STORY_LIST_LIMIT } from '../../../config'; import { STORY_LIST_LIMIT } from '../../../config';
import { buildCollectionByCallback } from '../../../util/iteratees'; import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { import {
buildApiPeerStories,
buildApiStealthMode, buildApiStealthMode,
buildApiStory, buildApiStoryView, buildApiUsersStories, buildApiStory,
buildApiStoryView,
} from '../apiBuilders/stories'; } from '../apiBuilders/stories';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
import { import {
buildInputEntity,
buildInputPeer, buildInputPeer,
buildInputPeerFromLocalDb,
buildInputPrivacyRules, buildInputPrivacyRules,
buildInputReaction, buildInputReaction,
buildInputReportReason, buildInputReportReason,
@ -38,7 +45,8 @@ export async function fetchAllStories({
| { state: string; stealthMode: ApiStealthMode } | { state: string; stealthMode: ApiStealthMode }
| { | {
users: ApiUser[]; users: ApiUser[];
userStories: Record<string, ApiUserStories>; chats: ApiChat[];
peerStories: Record<string, ApiPeerStories>;
hasMore?: true; hasMore?: true;
state: string; state: string;
stealthMode: ApiStealthMode; stealthMode: ApiStealthMode;
@ -60,13 +68,14 @@ export async function fetchAllStories({
} }
addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.users);
result.userStories.forEach((userStories) => ( addEntitiesToLocalDb(result.chats);
userStories.stories.forEach((story) => addStoryToLocalDb(story, buildApiPeerId(userStories.userId, 'user'))) result.peerStories.forEach((peerStories) => (
peerStories.stories.forEach((story) => addStoryToLocalDb(story, getApiChatIdFromMtpPeer(peerStories.peer)))
)); ));
const allUserStories = result.userStories.reduce<Record<string, ApiUserStories>>((acc, userStories) => { const allUserStories = result.peerStories.reduce<Record<string, ApiPeerStories>>((acc, peerStories) => {
const userId = buildApiPeerId(userStories.userId, 'user'); const peerId = getApiChatIdFromMtpPeer(peerStories.peer);
const stories = buildApiUsersStories(userStories); const stories = buildApiPeerStories(peerStories);
const { pinnedIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce< const { pinnedIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce<
{ {
pinnedIds: number[]; pinnedIds: number[];
@ -93,12 +102,12 @@ export async function fetchAllStories({
return acc; return acc;
} }
acc[userId] = { acc[peerId] = {
byId: stories, byId: stories,
orderedIds, orderedIds,
pinnedIds, pinnedIds,
lastUpdatedAt, lastUpdatedAt,
lastReadId: userStories.maxReadId, lastReadId: peerStories.maxReadId,
}; };
return acc; return acc;
@ -106,20 +115,21 @@ export async function fetchAllStories({
return { return {
users: result.users.map(buildApiUser).filter(Boolean), users: result.users.map(buildApiUser).filter(Boolean),
userStories: allUserStories, chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
peerStories: allUserStories,
hasMore: result.hasMore, hasMore: result.hasMore,
state: result.state, state: result.state,
stealthMode: buildApiStealthMode(result.stealthMode), stealthMode: buildApiStealthMode(result.stealthMode),
}; };
} }
export async function fetchUserStories({ export async function fetchPeerStories({
user, peer,
}: { }: {
user: ApiUser; peer: ApiPeer;
}) { }) {
const result = await invokeRequest(new GramJs.stories.GetUserStories({ const result = await invokeRequest(new GramJs.stories.GetPeerStories({
userId: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
})); }));
if (!result) { if (!result) {
@ -127,55 +137,58 @@ export async function fetchUserStories({
} }
addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.users);
result.stories.stories.forEach((story) => addStoryToLocalDb(story, user.id)); result.stories.stories.forEach((story) => addStoryToLocalDb(story, peer.id));
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const stories = buildCollectionByCallback(result.stories.stories, (story) => ( const stories = buildCollectionByCallback(result.stories.stories, (story) => (
[story.id, buildApiStory(user.id, story)] [story.id, buildApiStory(peer.id, story)]
)); ));
return { return {
chats,
users, users,
stories, stories,
lastReadStoryId: result.stories.maxReadId, lastReadStoryId: result.stories.maxReadId,
}; };
} }
export function fetchUserPinnedStories({ export function fetchPeerPinnedStories({
user, offsetId, peer, offsetId,
}: { }: {
user: ApiUser; peer: ApiPeer;
offsetId?: number; offsetId?: number;
}) { }) {
return fetchCommonStoriesRequest({ return fetchCommonStoriesRequest({
method: new GramJs.stories.GetPinnedStories({ method: new GramJs.stories.GetPinnedStories({
userId: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
offsetId, offsetId,
limit: STORY_LIST_LIMIT, limit: STORY_LIST_LIMIT,
}), }),
userId: user.id, peerId: peer.id,
}); });
} }
export function fetchStoriesArchive({ export function fetchStoriesArchive({
currentUserId, peer,
offsetId, offsetId,
}: { }: {
currentUserId: string; peer: ApiPeer;
offsetId?: number; offsetId?: number;
}) { }) {
return fetchCommonStoriesRequest({ return fetchCommonStoriesRequest({
method: new GramJs.stories.GetStoriesArchive({ method: new GramJs.stories.GetStoriesArchive({
peer: peer && buildInputPeer(peer.id, peer.accessHash),
offsetId, offsetId,
limit: STORY_LIST_LIMIT, limit: STORY_LIST_LIMIT,
}), }),
userId: currentUserId, peerId: peer.id,
}); });
} }
export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids: number[] }) { export async function fetchPeerStoriesByIds({ peer, ids }: { peer: ApiPeer; ids: number[] }) {
const result = await invokeRequest(new GramJs.stories.GetStoriesByID({ const result = await invokeRequest(new GramJs.stories.GetStoriesByID({
userId: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
id: ids, id: ids,
})); }));
@ -184,17 +197,19 @@ export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids:
} }
addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.users);
result.stories.forEach((story) => addStoryToLocalDb(story, user.id)); addEntitiesToLocalDb(result.chats);
result.stories.forEach((story) => addStoryToLocalDb(story, peer.id));
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const stories = ids.reduce<Record<string, ApiTypeStory>>((acc, id) => { const stories = ids.reduce<Record<string, ApiTypeStory>>((acc, id) => {
const story = result.stories.find(({ id: currentId }) => currentId === id); const story = result.stories.find(({ id: currentId }) => currentId === id);
if (story) { if (story) {
acc[id] = buildApiStory(user.id, story); acc[id] = buildApiStory(peer.id, story);
} else { } else {
acc[id] = { acc[id] = {
id, id,
userId: user.id, peerId: peer.id,
isDeleted: true, isDeleted: true,
}; };
} }
@ -203,34 +218,43 @@ export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids:
}, {}); }, {});
return { return {
chats,
users, users,
stories, stories,
}; };
} }
export function viewStory({ user, storyId }: { user: ApiUser; storyId: number }) { export function viewStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
return invokeRequest(new GramJs.stories.IncrementStoryViews({ return invokeRequest(new GramJs.stories.IncrementStoryViews({
userId: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId], id: [storyId],
})); }));
} }
export function markStoryRead({ user, storyId }: { user: ApiUser; storyId: number }) { export function markStoryRead({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
return invokeRequest(new GramJs.stories.ReadStories({ return invokeRequest(new GramJs.stories.ReadStories({
userId: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
maxId: storyId, maxId: storyId,
})); }));
} }
export function deleteStory({ storyId }: { storyId: number }) { export function deleteStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
return invokeRequest(new GramJs.stories.DeleteStories({ id: [storyId] })); return invokeRequest(new GramJs.stories.DeleteStories({
peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId],
}));
} }
export function toggleStoryPinned({ storyId, isPinned }: { storyId: number; isPinned?: boolean }) { export function toggleStoryPinned({ peer, storyId, isPinned }: { peer: ApiPeer; storyId: number; isPinned?: boolean }) {
return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned })); return invokeRequest(new GramJs.stories.TogglePinned({
peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId],
pinned: isPinned,
}));
} }
export async function fetchStoryViewList({ export async function fetchStoryViewList({
peer,
storyId, storyId,
areJustContacts, areJustContacts,
query, query,
@ -238,6 +262,7 @@ export async function fetchStoryViewList({
limit = STORY_LIST_LIMIT, limit = STORY_LIST_LIMIT,
offset = '', offset = '',
}: { }: {
peer: ApiPeer;
storyId: number; storyId: number;
areJustContacts?: true; areJustContacts?: true;
areReactionsFirst?: true; areReactionsFirst?: true;
@ -246,6 +271,7 @@ export async function fetchStoryViewList({
offset?: string; offset?: string;
}) { }) {
const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({ const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({
peer: buildInputPeer(peer.id, peer.accessHash),
id: storyId, id: storyId,
justContacts: areJustContacts, justContacts: areJustContacts,
q: query, q: query,
@ -271,14 +297,9 @@ export async function fetchStoryViewList({
}; };
} }
export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) { export async function fetchStoryLink({ peer, storyId }: { peer: ApiPeer ; storyId: number }) {
const inputUser = buildInputPeerFromLocalDb(userId);
if (!inputUser) {
return undefined;
}
const result = await invokeRequest(new GramJs.stories.ExportStoryLink({ const result = await invokeRequest(new GramJs.stories.ExportStoryLink({
userId: inputUser, peer: buildInputPeer(peer.id, peer.accessHash),
id: storyId, id: storyId,
})); }));
@ -290,15 +311,15 @@ export async function fetchStoryLink({ userId, storyId }: { userId: string; stor
} }
export function reportStory({ export function reportStory({
user, peer,
storyId, storyId,
reason, reason,
description, description,
}: { }: {
user: ApiUser; storyId: number; reason: ApiReportReason; description?: string; peer: ApiPeer; storyId: number; reason: ApiReportReason; description?: string;
}) { }) {
return invokeRequest(new GramJs.stories.Report({ return invokeRequest(new GramJs.stories.Report({
userId: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId], id: [storyId],
reason: buildInputReportReason(reason), reason: buildInputReportReason(reason),
message: description, message: description,
@ -306,13 +327,16 @@ export function reportStory({
} }
export function editStoryPrivacy({ export function editStoryPrivacy({
peer,
id, id,
privacy, privacy,
}: { }: {
peer: ApiPeer;
id: number; id: number;
privacy: ApiInputPrivacyRules; privacy: ApiInputPrivacyRules;
}) { }) {
return invokeRequest(new GramJs.stories.EditStory({ return invokeRequest(new GramJs.stories.EditStory({
peer: buildInputPeer(peer.id, peer.accessHash),
id, id,
privacyRules: buildInputPrivacyRules(privacy), privacyRules: buildInputPrivacyRules(privacy),
}), { }), {
@ -321,31 +345,31 @@ export function editStoryPrivacy({
} }
export function toggleStoriesHidden({ export function toggleStoriesHidden({
user, peer,
isHidden, isHidden,
}: { }: {
user: ApiUser; peer: ApiPeer;
isHidden: boolean; isHidden: boolean;
}) { }) {
return invokeRequest(new GramJs.contacts.ToggleStoriesHidden({ return invokeRequest(new GramJs.stories.TogglePeerStoriesHidden({
id: buildInputPeer(user.id, user.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
hidden: isHidden, hidden: isHidden,
})); }));
} }
export function fetchStoriesMaxIds({ export function fetchStoriesMaxIds({
users, peers,
}: { }: {
users: ApiUser[]; peers: ApiPeer[];
}) { }) {
return invokeRequest(new GramJs.users.GetStoriesMaxIDs({ return invokeRequest(new GramJs.stories.GetPeerMaxIDs({
id: users.map((user) => buildInputPeer(user.id, user.accessHash)), id: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
})); }));
} }
async function fetchCommonStoriesRequest({ method, userId }: { async function fetchCommonStoriesRequest({ method, peerId }: {
method: GramJs.stories.GetPinnedStories | GramJs.stories.GetStoriesArchive; method: GramJs.stories.GetPinnedStories | GramJs.stories.GetStoriesArchive;
userId: string; peerId: string;
}) { }) {
const result = await invokeRequest(method); const result = await invokeRequest(method);
@ -354,30 +378,33 @@ async function fetchCommonStoriesRequest({ method, userId }: {
} }
addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.users);
result.stories.forEach((story) => addStoryToLocalDb(story, userId)); addEntitiesToLocalDb(result.chats);
result.stories.forEach((story) => addStoryToLocalDb(story, peerId));
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const stories = buildCollectionByCallback(result.stories, (story) => ( const stories = buildCollectionByCallback(result.stories, (story) => (
[story.id, buildApiStory(userId, story)] [story.id, buildApiStory(peerId, story)]
)); ));
return { return {
users, users,
chats,
stories, stories,
}; };
} }
export function sendStoryReaction({ export function sendStoryReaction({
user, storyId, reaction, shouldAddToRecent, peer, storyId, reaction, shouldAddToRecent,
}: { }: {
user: ApiUser; peer: ApiPeer;
storyId: number; storyId: number;
reaction?: ApiReaction; reaction?: ApiReaction;
shouldAddToRecent?: boolean; shouldAddToRecent?: boolean;
}) { }) {
return invokeRequest(new GramJs.stories.SendReaction({ return invokeRequest(new GramJs.stories.SendReaction({
reaction: reaction ? buildInputReaction(reaction) : new GramJs.ReactionEmpty(), reaction: reaction ? buildInputReaction(reaction) : new GramJs.ReactionEmpty(),
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, peer: buildInputPeer(peer.id, peer.accessHash),
storyId, storyId,
...(shouldAddToRecent && { addToRecent: true }), ...(shouldAddToRecent && { addToRecent: true }),
}), { }), {

View File

@ -2,7 +2,7 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiChat, ApiSticker, ApiChat, ApiPeer, ApiSticker,
ApiUser, OnApiUpdate, ApiUser, OnApiUpdate,
} from '../../types'; } from '../../types';
@ -285,7 +285,7 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) {
}; };
} }
export function reportSpam(userOrChat: ApiUser | ApiChat) { export function reportSpam(userOrChat: ApiPeer) {
const { id, accessHash } = userOrChat; const { id, accessHash } = userOrChat;
return invokeRequest(new GramJs.messages.ReportSpam({ return invokeRequest(new GramJs.messages.ReportSpam({

View File

@ -1070,32 +1070,32 @@ export function updater(update: Update) {
} }
const { story } = update; const { story } = update;
const userId = buildApiPeerId(update.userId, 'user'); const peerId = getApiChatIdFromMtpPeer(update.peer);
addStoryToLocalDb(story, userId); addStoryToLocalDb(story, peerId);
if (story instanceof GramJs.StoryItemDeleted) { if (story instanceof GramJs.StoryItemDeleted) {
onUpdate({ onUpdate({
'@type': 'deleteStory', '@type': 'deleteStory',
userId, peerId,
storyId: story.id, storyId: story.id,
}); });
} else { } else {
onUpdate({ onUpdate({
'@type': 'updateStory', '@type': 'updateStory',
userId, peerId,
story: buildApiStory(userId, story) as ApiStory | ApiStorySkipped, story: buildApiStory(peerId, story) as ApiStory | ApiStorySkipped,
}); });
} }
} else if (update instanceof GramJs.UpdateReadStories) { } else if (update instanceof GramJs.UpdateReadStories) {
onUpdate({ onUpdate({
'@type': 'updateReadStories', '@type': 'updateReadStories',
userId: buildApiPeerId(update.userId, 'user'), peerId: getApiChatIdFromMtpPeer(update.peer),
lastReadId: update.maxId, lastReadId: update.maxId,
}); });
} else if (update instanceof GramJs.UpdateSentStoryReaction) { } else if (update instanceof GramJs.UpdateSentStoryReaction) {
onUpdate({ onUpdate({
'@type': 'updateSentStoryReaction', '@type': 'updateSentStoryReaction',
userId: buildApiPeerId(update.userId, 'user'), peerId: getApiChatIdFromMtpPeer(update.peer),
storyId: update.storyId, storyId: update.storyId,
reaction: buildApiReaction(update.reaction), reaction: buildApiReaction(update.reaction),
}); });

View File

@ -3,7 +3,7 @@ import type {
ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet, ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet,
} from './messages'; } from './messages';
import type { ApiChatInviteImporter } from './misc'; import type { ApiChatInviteImporter } from './misc';
import type { ApiFakeType, ApiUsername } from './users'; import type { ApiFakeType, ApiUser, ApiUsername } from './users';
type ApiChatType = ( type ApiChatType = (
'chatTypePrivate' | 'chatTypeSecret' | 'chatTypePrivate' | 'chatTypeSecret' |
@ -11,6 +11,8 @@ type ApiChatType = (
'chatTypeChannel' 'chatTypeChannel'
); );
export type ApiPeer = ApiChat | ApiUser;
export interface ApiChat { export interface ApiChat {
id: string; id: string;
folderId?: number; folderId?: number;
@ -23,7 +25,7 @@ export interface ApiChat {
unreadCount?: number; unreadCount?: number;
unreadMentionsCount?: number; unreadMentionsCount?: number;
unreadReactionsCount?: number; unreadReactionsCount?: number;
isVerified?: boolean; isVerified?: true;
isMuted?: boolean; isMuted?: boolean;
muteUntil?: number; muteUntil?: number;
isSignaturesShown?: boolean; isSignaturesShown?: boolean;
@ -35,7 +37,7 @@ export interface ApiChat {
usernames?: ApiUsername[]; usernames?: ApiUsername[];
membersCount?: number; membersCount?: number;
joinDate?: number; joinDate?: number;
isSupport?: boolean; isSupport?: true;
photos?: ApiPhoto[]; photos?: ApiPhoto[];
draftDate?: number; draftDate?: number;
isProtected?: boolean; isProtected?: boolean;
@ -77,6 +79,12 @@ export interface ApiChat {
unreadReactions?: number[]; unreadReactions?: number[];
unreadMentions?: number[]; unreadMentions?: number[];
// Stories
areStoriesHidden?: boolean;
hasStories?: boolean;
hasUnreadStories?: boolean;
maxStoryId?: number;
// Locally determined field // Locally determined field
detectedLanguage?: string; detectedLanguage?: string;
} }
@ -118,6 +126,7 @@ export interface ApiChatFullInfo {
profilePhoto?: ApiPhoto; profilePhoto?: ApiPhoto;
areParticipantsHidden?: boolean; areParticipantsHidden?: boolean;
isTranslationDisabled?: true; isTranslationDisabled?: true;
hasPinnedStories?: boolean;
} }
export interface ApiChatMember { export interface ApiChatMember {
@ -145,6 +154,9 @@ export interface ApiChatAdminRights {
anonymous?: true; anonymous?: true;
manageCall?: true; manageCall?: true;
manageTopics?: true; manageTopics?: true;
postStories?: true;
editStories?: true;
deleteStories?: true;
} }
export interface ApiChatBannedRights { export interface ApiChatBannedRights {

View File

@ -194,7 +194,7 @@ export interface ApiInvoice {
receiptMsgId?: number; receiptMsgId?: number;
isTest?: boolean; isTest?: boolean;
isRecurring?: boolean; isRecurring?: boolean;
recurringTermsUrl?: string; termsUrl?: string;
extendedMedia?: ApiMessageExtendedMediaPreview; extendedMedia?: ApiMessageExtendedMediaPreview;
maxTipAmount?: number; maxTipAmount?: number;
suggestedTipAmounts?: number[]; suggestedTipAmounts?: number[];

View File

@ -1,10 +1,12 @@
import type { ApiPrivacySettings } from '../../types'; import type { ApiPrivacySettings } from '../../types';
import type { ApiGeoPoint, ApiMessage, ApiReaction } from './messages'; import type {
ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount,
} from './messages';
export interface ApiStory { export interface ApiStory {
'@type'?: 'story'; '@type'?: 'story';
id: number; id: number;
userId: string; peerId: string;
date: number; date: number;
expireDate: number; expireDate: number;
content: ApiMessage['content']; content: ApiMessage['content'];
@ -14,9 +16,11 @@ export interface ApiStory {
isForContacts?: boolean; isForContacts?: boolean;
isForSelectedContacts?: boolean; isForSelectedContacts?: boolean;
isPublic?: boolean; isPublic?: boolean;
isOut?: true;
noForwards?: boolean; noForwards?: boolean;
viewsCount?: number; viewsCount?: number;
reactionsCount?: number; reactionsCount?: number;
reactions?: ApiReactionCount[];
recentViewerIds?: string[]; recentViewerIds?: string[];
visibility?: ApiPrivacySettings; visibility?: ApiPrivacySettings;
sentReaction?: ApiReaction; sentReaction?: ApiReaction;
@ -26,7 +30,7 @@ export interface ApiStory {
export interface ApiStorySkipped { export interface ApiStorySkipped {
'@type'?: 'storySkipped'; '@type'?: 'storySkipped';
id: number; id: number;
userId: string; peerId: string;
isForCloseFriends?: boolean; isForCloseFriends?: boolean;
date: number; date: number;
expireDate: number; expireDate: number;
@ -35,15 +39,15 @@ export interface ApiStorySkipped {
export interface ApiStoryDeleted { export interface ApiStoryDeleted {
'@type'?: 'storyDeleted'; '@type'?: 'storyDeleted';
id: number; id: number;
userId: string; peerId: string;
isDeleted: true; isDeleted: true;
} }
export type ApiTypeStory = ApiStory | ApiStorySkipped | ApiStoryDeleted; export type ApiTypeStory = ApiStory | ApiStorySkipped | ApiStoryDeleted;
export type ApiUserStories = { export type ApiPeerStories = {
byId: Record<number, ApiTypeStory>; byId: Record<number, ApiTypeStory>;
orderedIds: number[]; // Actual user stories orderedIds: number[]; // Actual peer stories
pinnedIds: number[]; // Profile Shared Media: Pinned Stories tab pinnedIds: number[]; // Profile Shared Media: Pinned Stories tab
archiveIds?: number[]; // Profile Shared Media: Archive Stories tab archiveIds?: number[]; // Profile Shared Media: Archive Stories tab
lastUpdatedAt?: number; lastUpdatedAt?: number;
@ -52,13 +56,13 @@ export type ApiUserStories = {
export type ApiMessageStoryData = { export type ApiMessageStoryData = {
id: number; id: number;
userId: string; peerId: string;
isMention?: boolean; isMention?: boolean;
}; };
export type ApiWebPageStoryData = { export type ApiWebPageStoryData = {
id: number; id: number;
userId: string; peerId: string;
}; };
export type ApiStoryView = { export type ApiStoryView = {
@ -95,4 +99,12 @@ export type ApiMediaAreaGeoPoint = {
geo: ApiGeoPoint; geo: ApiGeoPoint;
}; };
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint; export type ApiMediaAreaSuggestedReaction = {
type: 'suggestedReaction';
coordinates: ApiMediaAreaCoordinates;
reaction: ApiReaction;
isDark?: boolean;
isFlipped?: boolean;
};
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction;

View File

@ -629,25 +629,25 @@ export type ApiRequestReconnectApi = {
export type ApiUpdateStory = { export type ApiUpdateStory = {
'@type': 'updateStory'; '@type': 'updateStory';
userId: string; peerId: string;
story: ApiStory | ApiStorySkipped; story: ApiStory | ApiStorySkipped;
}; };
export type ApiUpdateDeleteStory = { export type ApiUpdateDeleteStory = {
'@type': 'deleteStory'; '@type': 'deleteStory';
userId: string; peerId: string;
storyId: number; storyId: number;
}; };
export type ApiUpdateReadStories = { export type ApiUpdateReadStories = {
'@type': 'updateReadStories'; '@type': 'updateReadStories';
userId: string; peerId: string;
lastReadId: number; lastReadId: number;
}; };
export type ApiUpdateSentStoryReaction = { export type ApiUpdateSentStoryReaction = {
'@type': 'updateSentStoryReaction'; '@type': 'updateSentStoryReaction';
userId: string; peerId: string;
storyId: number; storyId: number;
reaction?: ApiReaction; reaction?: ApiReaction;
}; };

View File

@ -4,7 +4,7 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global'; import { withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types'; import type { ApiPeer } from '../../../api/types';
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
import { GROUP_CALL_DEFAULT_VOLUME } from '../../../config'; import { GROUP_CALL_DEFAULT_VOLUME } from '../../../config';
@ -31,7 +31,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
peer?: ApiUser | ApiChat; peer?: ApiPeer;
}; };
const GroupCallParticipant: FC<OwnProps & StateProps> = ({ const GroupCallParticipant: FC<OwnProps & StateProps> = ({

View File

@ -6,6 +6,7 @@ import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../
import { selectActiveGroupCall } from '../../../global/selectors/calls'; import { selectActiveGroupCall } from '../../../global/selectors/calls';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { compareFields } from '../../../util/iteratees';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -72,10 +73,6 @@ const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
); );
}; };
function compareFields<T>(a: T, b: T) {
return Number(b) - Number(a);
}
function compareParticipants(a: TypeGroupCallParticipant, b: TypeGroupCallParticipant) { function compareParticipants(a: TypeGroupCallParticipant, b: TypeGroupCallParticipant) {
return compareFields(!a.isMuted, !b.isMuted) return compareFields(!a.isMuted, !b.isMuted)
|| compareFields(a.presentation, b.presentation) || compareFields(a.presentation, b.presentation)

View File

@ -3,7 +3,9 @@ import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { memo, useRef } from '../../lib/teact/teact'; import React, { memo, useRef } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; import type {
ApiChat, ApiPeer, ApiPhoto, ApiUser,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { StoryViewerOrigin } from '../../types'; import type { StoryViewerOrigin } from '../../types';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
@ -13,8 +15,8 @@ import {
getChatAvatarHash, getChatAvatarHash,
getChatTitle, getChatTitle,
getPeerColorKey, getPeerColorKey,
getPeerStoryHtmlId,
getUserFullName, getUserFullName,
getUserStoryHtmlId,
isChatWithRepliesBot, isChatWithRepliesBot,
isDeletedUser, isDeletedUser,
isUserId, isUserId,
@ -45,7 +47,7 @@ cn.icon = cn('icon');
type OwnProps = { type OwnProps = {
className?: string; className?: string;
size?: AvatarSize; size?: AvatarSize;
peer?: ApiChat | ApiUser; peer?: ApiPeer;
photo?: ApiPhoto; photo?: ApiPhoto;
text?: string; text?: string;
isSavedMessages?: boolean; isSavedMessages?: boolean;
@ -55,7 +57,7 @@ type OwnProps = {
withStoryGap?: boolean; withStoryGap?: boolean;
withStorySolid?: boolean; withStorySolid?: boolean;
storyViewerOrigin?: StoryViewerOrigin; storyViewerOrigin?: StoryViewerOrigin;
storyViewerMode?: 'full' | 'single-user' | 'disabled'; storyViewerMode?: 'full' | 'single-peer' | 'disabled';
loopIndefinitely?: boolean; loopIndefinitely?: boolean;
noPersonalPhoto?: boolean; noPersonalPhoto?: boolean;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
@ -75,7 +77,7 @@ const Avatar: FC<OwnProps> = ({
withStoryGap, withStoryGap,
withStorySolid, withStorySolid,
storyViewerOrigin, storyViewerOrigin,
storyViewerMode = 'single-user', storyViewerMode = 'single-peer',
loopIndefinitely, loopIndefinitely,
noPersonalPhoto, noPersonalPhoto,
onClick, onClick,
@ -213,9 +215,9 @@ const Avatar: FC<OwnProps> = ({
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isReplies && 'replies-bot-account', isReplies && 'replies-bot-account',
isForum && 'forum', isForum && 'forum',
((withStory && user?.hasStories) || forPremiumPromo) && 'with-story-circle', ((withStory && peer?.hasStories) || forPremiumPromo) && 'with-story-circle',
withStorySolid && user?.hasStories && 'with-story-solid', withStorySolid && peer?.hasStories && 'with-story-solid',
withStorySolid && user?.hasUnreadStories && 'has-unread-story', withStorySolid && peer?.hasUnreadStories && 'has-unread-story',
onClick && 'interactive', onClick && 'interactive',
(!isSavedMessages && !imgBlobUrl) && 'no-photo', (!isSavedMessages && !imgBlobUrl) && 'no-photo',
); );
@ -223,12 +225,12 @@ const Avatar: FC<OwnProps> = ({
const hasMedia = Boolean(isSavedMessages || imgBlobUrl); const hasMedia = Boolean(isSavedMessages || imgBlobUrl);
const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => { const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
if (withStory && storyViewerMode !== 'disabled' && user?.hasStories) { if (withStory && storyViewerMode !== 'disabled' && peer?.hasStories) {
e.stopPropagation(); e.stopPropagation();
openStoryViewer({ openStoryViewer({
userId: user.id, peerId: peer.id,
isSingleUser: storyViewerMode === 'single-user', isSinglePeer: storyViewerMode === 'single-peer',
origin: storyViewerOrigin, origin: storyViewerOrigin,
}); });
return; return;
@ -243,7 +245,7 @@ const Avatar: FC<OwnProps> = ({
<div <div
ref={ref} ref={ref}
className={fullClassName} className={fullClassName}
id={peer?.id && withStory ? getUserStoryHtmlId(peer.id) : undefined} id={peer?.id && withStory ? getPeerStoryHtmlId(peer.id) : undefined}
data-peer-id={peer?.id} data-peer-id={peer?.id}
data-test-sender-id={IS_TEST ? peer?.id : undefined} data-test-sender-id={IS_TEST ? peer?.id : undefined}
aria-label={typeof content === 'string' ? author : undefined} aria-label={typeof content === 'string' ? author : undefined}
@ -253,8 +255,8 @@ const Avatar: FC<OwnProps> = ({
<div className="inner"> <div className="inner">
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content} {typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
</div> </div>
{withStory && user?.hasStories && ( {withStory && peer?.hasStories && (
<AvatarStoryCircle userId={user.id} size={size} withExtraGap={withStoryGap} /> <AvatarStoryCircle peerId={peer.id} size={size} withExtraGap={withStoryGap} />
)} )}
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact';
import type { ApiChat, ApiUser } from '../../api/types'; import type { ApiPeer } from '../../api/types';
import type { AvatarSize } from './Avatar'; import type { AvatarSize } from './Avatar';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -14,7 +14,7 @@ import styles from './AvatarList.module.scss';
type OwnProps = { type OwnProps = {
size: AvatarSize; size: AvatarSize;
peers?: (ApiUser | ApiChat)[]; peers?: ApiPeer[];
className?: string; className?: string;
}; };

View File

@ -6,14 +6,14 @@ import { withGlobal } from '../../global';
import type { ThemeKey } from '../../types'; import type { ThemeKey } from '../../types';
import type { AvatarSize } from './Avatar'; import type { AvatarSize } from './Avatar';
import { selectTheme, selectUser, selectUserStories } from '../../global/selectors'; import { selectPeerStories, selectTheme, selectUser } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { DPR } from '../../util/windowEnvironment'; import { DPR } from '../../util/windowEnvironment';
import { REM } from './helpers/mediaDimensions'; import { REM } from './helpers/mediaDimensions';
interface OwnProps { interface OwnProps {
// eslint-disable-next-line react/no-unused-prop-types // eslint-disable-next-line react/no-unused-prop-types
userId: string; peerId: string;
className?: string; className?: string;
size: AvatarSize; size: AvatarSize;
withExtraGap?: boolean; withExtraGap?: boolean;
@ -108,15 +108,15 @@ function AvatarStoryCircle({
); );
} }
export default memo(withGlobal<OwnProps>((global, { userId }): StateProps => { export default memo(withGlobal<OwnProps>((global, { peerId }): StateProps => {
const user = selectUser(global, userId); const user = selectUser(global, peerId);
const userStories = selectUserStories(global, userId); const peerStories = selectPeerStories(global, peerId);
const appTheme = selectTheme(global); const appTheme = selectTheme(global);
return { return {
isCloseFriend: user?.isCloseFriend, isCloseFriend: user?.isCloseFriend,
storyIds: userStories?.orderedIds, storyIds: peerStories?.orderedIds,
lastReadId: userStories?.lastReadId, lastReadId: peerStories?.lastReadId,
appTheme, appTheme,
}; };
})(AvatarStoryCircle)); })(AvatarStoryCircle));

View File

@ -34,6 +34,7 @@ import { debounce } from '../../util/schedulers';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
@ -77,7 +78,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
showNotification, showNotification,
updateChatMutedState, updateChatMutedState,
updateTopicMutedState, updateTopicMutedState,
loadUserStories, loadPeerStories,
} = getActions(); } = getActions();
const { const {
@ -87,6 +88,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
isSelf, isSelf,
} = user || {}; } = user || {};
const { id: chatId, usernames: chatUsernames } = chat || {}; const { id: chatId, usernames: chatUsernames } = chat || {};
const peerId = userId || chatId;
const lang = useLang(); const lang = useLang();
const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted); const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted);
@ -98,9 +100,15 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (!userId) return; if (!userId) return;
loadFullUser({ userId }); loadFullUser({ userId });
loadUserStories({ userId });
}, [userId]); }, [userId]);
useEffectWithPrevDeps(([prevPeerId]) => {
if (!peerId || prevPeerId === peerId) return;
if (user || (chat && isChatChannel(chat))) {
loadPeerStories({ peerId });
}
}, [peerId, chat, user]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
const activeUsernames = useMemo(() => { const activeUsernames = useMemo(() => {

View File

@ -34,6 +34,7 @@ import type { IAnchorPosition, InlineBotSettings, ISettings } from '../../types'
import { import {
BASE_EMOJI_KEYWORD_LANG, BASE_EMOJI_KEYWORD_LANG,
EDITABLE_INPUT_MODAL_ID, EDITABLE_INPUT_MODAL_ID,
HEART_REACTION,
MAX_UPLOAD_FILEPART_SIZE, MAX_UPLOAD_FILEPART_SIZE,
REPLIES_USER_ID, REPLIES_USER_ID,
SCHEDULED_WHEN_ONLINE, SCHEDULED_WHEN_ONLINE,
@ -68,6 +69,7 @@ import {
selectIsReactionPickerOpen, selectIsReactionPickerOpen,
selectIsRightColumnShown, selectIsRightColumnShown,
selectNewestMessageWithBotKeyboardButtons, selectNewestMessageWithBotKeyboardButtons,
selectPeerStory,
selectReplyingToId, selectReplyingToId,
selectRequestedDraftFiles, selectRequestedDraftFiles,
selectRequestedDraftText, selectRequestedDraftText,
@ -76,7 +78,6 @@ import {
selectTheme, selectTheme,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
selectUserStory,
} from '../../global/selectors'; } from '../../global/selectors';
import { selectCurrentLimit } from '../../global/selectors/limits'; import { selectCurrentLimit } from '../../global/selectors/limits';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -261,10 +262,6 @@ const MESSAGE_MAX_LENGTH = 4096;
const SENDING_ANIMATION_DURATION = 350; const SENDING_ANIMATION_DURATION = 350;
const MOUNT_ANIMATION_DURATION = 430; const MOUNT_ANIMATION_DURATION = 430;
const HEART_REACTION: ApiReaction = {
emoticon: '❤',
};
const Composer: FC<OwnProps & StateProps> = ({ const Composer: FC<OwnProps & StateProps> = ({
type, type,
isOnActiveTab, isOnActiveTab,
@ -779,7 +776,7 @@ const Composer: FC<OwnProps & StateProps> = ({
if (storyReactionPickerPosition) { if (storyReactionPickerPosition) {
openStoryReactionPicker({ openStoryReactionPicker({
storyUserId: chatId, peerId: chatId,
storyId: storyId!, storyId: storyId!,
position: storyReactionPickerPosition, position: storyReactionPickerPosition,
}); });
@ -1412,7 +1409,7 @@ const Composer: FC<OwnProps & StateProps> = ({
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => { const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
openStoryReactionPicker({ openStoryReactionPicker({
storyUserId: chatId, peerId: chatId,
storyId: storyId!, storyId: storyId!,
position, position,
sendAsMessage: true, sendAsMessage: true,
@ -1421,7 +1418,12 @@ const Composer: FC<OwnProps & StateProps> = ({
const handleLikeStory = useLastCallback(() => { const handleLikeStory = useLastCallback(() => {
const reaction = sentStoryReaction ? undefined : HEART_REACTION; const reaction = sentStoryReaction ? undefined : HEART_REACTION;
sendStoryReaction({ userId: chatId, storyId: storyId!, reaction }); sendStoryReaction({
peerId: chatId,
storyId: storyId!,
containerId: getStoryKey(chatId, storyId!),
reaction,
});
}); });
const handleSendScheduled = useLastCallback(() => { const handleSendScheduled = useLastCallback(() => {
@ -1816,6 +1818,7 @@ const Composer: FC<OwnProps & StateProps> = ({
> >
{sentStoryReaction && ( {sentStoryReaction && (
<ReactionAnimatedEmoji <ReactionAnimatedEmoji
key={'documentId' in sentStoryReaction ? sentStoryReaction.documentId : sentStoryReaction.emoticon}
containerId={getStoryKey(chatId, storyId!)} containerId={getStoryKey(chatId, storyId!)}
reaction={sentStoryReaction} reaction={sentStoryReaction}
withEffectOnly={isSentStoryReactionHeart} withEffectOnly={isSentStoryReactionHeart}
@ -1928,7 +1931,7 @@ export default memo(withGlobal<OwnProps>(
const replyingToId = selectReplyingToId(global, chatId, threadId); const replyingToId = selectReplyingToId(global, chatId, threadId);
const story = storyId && selectUserStory(global, chatId, storyId); const story = storyId && selectPeerStory(global, chatId, storyId);
const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined; const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined;
return { return {

View File

@ -41,6 +41,7 @@ type OwnProps = {
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
onClick?: NoneToVoidFunction; onClick?: NoneToVoidFunction;
onAnimationEnd?: NoneToVoidFunction;
}; };
const STICKER_SIZE = 20; const STICKER_SIZE = 20;
@ -65,6 +66,7 @@ const CustomEmoji: FC<OwnProps> = ({
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
onClick, onClick,
onAnimationEnd,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null); let containerRef = useRef<HTMLDivElement>(null);
@ -119,6 +121,7 @@ const CustomEmoji: FC<OwnProps> = ({
withGridFix && styles.withGridFix, withGridFix && styles.withGridFix,
)} )}
onClick={onClick} onClick={onClick}
onAnimationEnd={onAnimationEnd}
data-entity-type={ApiMessageEntityTypes.CustomEmoji} data-entity-type={ApiMessageEntityTypes.CustomEmoji}
data-document-id={documentId} data-document-id={documentId}
data-alt={customEmoji?.emoji} data-alt={customEmoji?.emoji}

View File

@ -2,8 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { useRef } from '../../lib/teact/teact'; import React, { useRef } from '../../lib/teact/teact';
import type { import type {
ApiChat, ApiMessage, ApiPeer,
ApiMessage, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { ChatTranslatedMessages } from '../../global/types'; import type { ChatTranslatedMessages } from '../../global/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
@ -37,7 +36,7 @@ import './EmbeddedMessage.scss';
type OwnProps = { type OwnProps = {
className?: string; className?: string;
message?: ApiMessage; message?: ApiMessage;
sender?: ApiUser | ApiChat; sender?: ApiPeer;
title?: string; title?: string;
customText?: string; customText?: string;
noUserColors?: boolean; noUserColors?: boolean;

View File

@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { useRef } from '../../lib/teact/teact'; import React, { useRef } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiTypeStory, ApiUser } from '../../api/types'; import type { ApiPeer, ApiTypeStory } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { import {
@ -24,7 +24,7 @@ import './EmbeddedMessage.scss';
type OwnProps = { type OwnProps = {
story?: ApiTypeStory; story?: ApiTypeStory;
sender?: ApiUser | ApiChat; sender?: ApiPeer;
noUserColors?: boolean; noUserColors?: boolean;
isProtected?: boolean; isProtected?: boolean;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;

View File

@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiUser } from '../../api/types'; import type { ApiChat, ApiPeer, ApiUser } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config'; import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
@ -23,7 +23,7 @@ import VerifiedIcon from './VerifiedIcon';
import styles from './FullNameTitle.module.scss'; import styles from './FullNameTitle.module.scss';
type OwnProps = { type OwnProps = {
peer: ApiChat | ApiUser; peer: ApiPeer;
className?: string; className?: string;
noVerified?: boolean; noVerified?: boolean;
noFake?: boolean; noFake?: boolean;

View File

@ -6,7 +6,7 @@ import type {
ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus,
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useLang'; import type { LangFn } from '../../hooks/useLang';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin, type StoryViewerOrigin } from '../../types';
import { import {
getChatTypeString, getChatTypeString,
@ -51,6 +51,8 @@ type OwnProps = {
noRtl?: boolean; noRtl?: boolean;
noAvatar?: boolean; noAvatar?: boolean;
noStatusOrTyping?: boolean; noStatusOrTyping?: boolean;
withStory?: boolean;
storyViewerOrigin?: StoryViewerOrigin;
onClick?: VoidFunction; onClick?: VoidFunction;
}; };
@ -84,6 +86,8 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
topic, topic,
messagesCount, messagesCount,
noStatusOrTyping, noStatusOrTyping,
withStory,
storyViewerOrigin,
onClick, onClick,
}) => { }) => {
const { const {
@ -186,6 +190,9 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
key={chat.id} key={chat.id}
size={avatarSize} size={avatarSize}
peer={chat} peer={chat}
withStory={withStory}
storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer"
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/> />
)} )}

View File

@ -0,0 +1,27 @@
import React from '../../lib/teact/teact';
import type { IconName } from '../../types/icons';
import buildClassName from '../../util/buildClassName';
type OwnProps = {
name: IconName;
className?: string;
style?: string;
};
const Icon = ({
name,
className,
style,
}: OwnProps) => {
return (
<i
className={buildClassName(`icon icon-${name}`, className)}
style={style}
aria-hidden
/>
);
};
export default Icon;

View File

@ -5,7 +5,7 @@ import { withGlobal } from '../../global';
import type { ApiChat, ApiUser } from '../../api/types'; import type { ApiChat, ApiUser } from '../../api/types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers'; import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
import { selectChat, selectUser } from '../../global/selectors'; import { selectChat, selectUser } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -111,8 +111,8 @@ export default memo(withGlobal<OwnProps>(
return {}; return {};
} }
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; const chat = selectChat(global, chatOrUserId);
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; const user = selectUser(global, chatOrUserId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
return { return {

View File

@ -188,7 +188,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
withStory={withStory} withStory={withStory}
storyViewerOrigin={storyViewerOrigin} storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-user" storyViewerMode="single-peer"
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/> />
<div className="info"> <div className="info">

View File

@ -18,8 +18,7 @@ import RadioGroup from '../ui/RadioGroup';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
subject?: 'peer' | 'messages' | 'media' | 'story'; subject?: 'peer' | 'messages' | 'media' | 'story';
chatId?: string; peerId?: string;
userId?: string;
photo?: ApiPhoto; photo?: ApiPhoto;
messageIds?: number[]; messageIds?: number[];
storyId?: number; storyId?: number;
@ -30,8 +29,7 @@ export type OwnProps = {
const ReportModal: FC<OwnProps> = ({ const ReportModal: FC<OwnProps> = ({
isOpen, isOpen,
subject = 'messages', subject = 'messages',
chatId, peerId,
userId,
photo, photo,
messageIds, messageIds,
storyId, storyId,
@ -56,16 +54,16 @@ const ReportModal: FC<OwnProps> = ({
exitMessageSelectMode(); exitMessageSelectMode();
break; break;
case 'peer': case 'peer':
reportPeer({ chatId, reason: selectedReason, description }); reportPeer({ chatId: peerId, reason: selectedReason, description });
break; break;
case 'media': case 'media':
reportProfilePhoto({ reportProfilePhoto({
chatId, photo, reason: selectedReason, description, chatId: peerId, photo, reason: selectedReason, description,
}); });
break; break;
case 'story': case 'story':
reportStory({ reportStory({
userId: userId!, storyId: storyId!, reason: selectedReason, description, peerId: peerId!, storyId: storyId!, reason: selectedReason, description,
}); });
} }
onClose(); onClose();
@ -94,9 +92,9 @@ const ReportModal: FC<OwnProps> = ({
if ( if (
(subject === 'messages' && !messageIds) (subject === 'messages' && !messageIds)
|| (subject === 'peer' && !chatId) || (subject === 'peer' && !peerId)
|| (subject === 'media' && (!chatId || !photo)) || (subject === 'media' && (!peerId || !photo))
|| (subject === 'story' && (!storyId || !userId)) || (subject === 'story' && (!storyId || !peerId))
) { ) {
return undefined; return undefined;
} }

View File

@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { useCallback } from '../../lib/teact/teact'; import React, { useCallback } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiUser } from '../../api/types'; import type { ApiPeer } from '../../api/types';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -10,7 +10,7 @@ import Link from '../ui/Link';
type OwnProps = { type OwnProps = {
className?: string; className?: string;
sender?: ApiUser | ApiChat; sender?: ApiPeer;
children: React.ReactNode; children: React.ReactNode;
}; };

View File

@ -4,11 +4,12 @@
} }
.particle { .particle {
--custom-emoji-size: var(--particle-size, 1rem);
color: var(--color-primary); color: var(--color-primary);
position: absolute; position: absolute;
width: 1rem; width: var(--particle-size, 1rem);
height: 1rem; height: var(--particle-size, 1rem);
border-radius: 0.25rem; border-radius: 0.25rem;
offset-path: var(--offset-path); offset-path: var(--offset-path);
offset-rotate: 0deg; offset-rotate: 0deg;

View File

@ -5,6 +5,7 @@ import type { ApiEmojiStatus, ApiReactionCustomEmoji } from '../../../api/types'
import { getStickerPreviewHash } from '../../../global/helpers'; import { getStickerPreviewHash } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment'; import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
@ -17,6 +18,8 @@ type OwnProps = {
reaction: ApiReactionCustomEmoji | ApiEmojiStatus; reaction: ApiReactionCustomEmoji | ApiEmojiStatus;
className?: string; className?: string;
isLottie?: boolean; isLottie?: boolean;
particleSize?: number;
onEnded?: NoneToVoidFunction;
}; };
const EFFECT_AMOUNT = 7; const EFFECT_AMOUNT = 7;
@ -25,6 +28,8 @@ const CustomEmojiEffect: FC<OwnProps> = ({
reaction, reaction,
isLottie, isLottie,
className, className,
particleSize,
onEnded,
}) => { }) => {
const stickerHash = getStickerPreviewHash(reaction.documentId); const stickerHash = getStickerPreviewHash(reaction.documentId);
@ -32,16 +37,19 @@ const CustomEmojiEffect: FC<OwnProps> = ({
const paths: string[] = useMemo(() => { const paths: string[] = useMemo(() => {
if (!IS_OFFSET_PATH_SUPPORTED) return []; if (!IS_OFFSET_PATH_SUPPORTED) return [];
return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath()); return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath(particleSize));
}, []); }, [particleSize]);
if (!previewMediaData && !isLottie) { if (!previewMediaData && !isLottie) {
return undefined; return undefined;
} }
return ( return (
<div className={buildClassName(styles.root, className)}> <div
{paths.map((path) => { className={buildClassName(styles.root, className)}
style={buildStyle(Boolean(particleSize) && `--particle-size: ${particleSize}px`)}
>
{paths.map((path, i) => {
const style = `--offset-path: path('${path}');`; const style = `--offset-path: path('${path}');`;
if (isLottie) { if (isLottie) {
return ( return (
@ -50,6 +58,8 @@ const CustomEmojiEffect: FC<OwnProps> = ({
className={styles.particle} className={styles.particle}
style={style} style={style}
withSharedAnimation withSharedAnimation
size={particleSize}
onAnimationEnd={i === 0 ? onEnded : undefined}
/> />
); );
} }
@ -61,6 +71,7 @@ const CustomEmojiEffect: FC<OwnProps> = ({
className={styles.particle} className={styles.particle}
style={style} style={style}
draggable={false} draggable={false}
onAnimationEnd={i === 0 ? onEnded : undefined}
/> />
); );
})} })}
@ -70,9 +81,9 @@ const CustomEmojiEffect: FC<OwnProps> = ({
export default memo(CustomEmojiEffect); export default memo(CustomEmojiEffect);
function generateRandomDropPath() { function generateRandomDropPath(particleSize = 20) {
const x = (10 + Math.random() * 60) * (Math.random() > 0.5 ? 1 : -1); const x = (particleSize / 2 + Math.random() * particleSize * 3) * (Math.random() > 0.5 ? 1 : -1);
const y = 20 + Math.random() * 80; const y = particleSize + Math.random() * particleSize * 4;
return `M 0 0 C 0 0 ${x} ${-y - 20} ${x} ${y}`; return `M 0 0 C 0 0 ${x} ${-y - particleSize} ${x} ${y}`;
} }

View File

@ -12,13 +12,6 @@
z-index: 1; z-index: 1;
} }
.animated-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.effect { .effect {
position: fixed; position: fixed;
top: -2.25rem; top: -2.25rem;
@ -44,7 +37,7 @@
contain: layout; contain: layout;
} }
.withEffectOnly { .withEffectOnly, .animated-icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;

View File

@ -33,6 +33,8 @@ type OwnProps = {
size?: number; size?: number;
effectSize?: number; effectSize?: number;
withEffectOnly?: boolean; withEffectOnly?: boolean;
shouldPause?: boolean;
shouldLoop?: boolean;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
}; };
@ -46,6 +48,8 @@ type StateProps = {
const ICON_SIZE = 1.5 * REM; const ICON_SIZE = 1.5 * REM;
const CENTER_ICON_MULTIPLIER = 1.9; const CENTER_ICON_MULTIPLIER = 1.9;
const EFFECT_SIZE = 6.5 * REM; const EFFECT_SIZE = 6.5 * REM;
const CUSTOM_EMOJI_EFFECT_MULTIPLIER = 0.5;
const MIN_PARTICLE_SIZE = REM;
const ReactionAnimatedEmoji = ({ const ReactionAnimatedEmoji = ({
containerId, containerId,
@ -58,6 +62,8 @@ const ReactionAnimatedEmoji = ({
genericEffects, genericEffects,
withEffects, withEffects,
withEffectOnly, withEffectOnly,
shouldPause,
shouldLoop,
observeIntersection, observeIntersection,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { stopActiveReaction } = getActions(); const { stopActiveReaction } = getActions();
@ -110,18 +116,25 @@ const ReactionAnimatedEmoji = ({
activeReactions?.find((active) => isSameReaction(active, reaction)) activeReactions?.find((active) => isSameReaction(active, reaction))
), [activeReactions, reaction]); ), [activeReactions, reaction]);
const shouldPlay = Boolean(withEffects && activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect); const shouldPlayEffect = Boolean(
withEffects && activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect,
);
const shouldPlayCenter = isIntersecting && ((shouldPlayEffect && !withEffectOnly) || shouldLoop);
const { const {
shouldRender: shouldRenderAnimation, shouldRender: shouldRenderEffect,
transitionClassNames: animationClassNames, transitionClassNames: animationClassNames,
} = useShowTransition(shouldPlay, undefined, true, 'slow'); } = useShowTransition(shouldPlayEffect, undefined, true, 'slow');
const {
shouldRender: shouldRenderCenter,
transitionClassNames: centerAnimationClassNames,
} = useShowTransition(shouldPlayCenter, undefined, true, 'slow');
const handleEnded = useLastCallback(() => { const handleEnded = useLastCallback(() => {
stopActiveReaction({ containerId, reaction }); stopActiveReaction({ containerId, reaction });
}); });
const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag(); const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag();
const shouldShowStatic = !isCustom && (!shouldPlay || !isAnimationLoaded); const shouldShowStatic = !isCustom && (!shouldPlayCenter || !isAnimationLoaded);
const { const {
shouldRender: shouldRenderStatic, shouldRender: shouldRenderStatic,
transitionClassNames: staticClassNames, transitionClassNames: staticClassNames,
@ -129,7 +142,7 @@ const ReactionAnimatedEmoji = ({
const rootClassName = buildClassName( const rootClassName = buildClassName(
styles.root, styles.root,
shouldRenderAnimation && styles.animating, shouldRenderEffect && styles.animating,
withEffectOnly && styles.withEffectOnly, withEffectOnly && styles.withEffectOnly,
className, className,
); );
@ -150,13 +163,28 @@ const ReactionAnimatedEmoji = ({
documentId={reaction.documentId} documentId={reaction.documentId}
className={styles.customEmoji} className={styles.customEmoji}
size={size} size={size}
noPlay={shouldPause}
forceAlways
observeIntersectionForPlaying={observeIntersection} observeIntersectionForPlaying={observeIntersection}
/> />
)} )}
{shouldRenderAnimation && ( {shouldRenderCenter && !isCustom && (
<AnimatedSticker
key={`${centerIconId}-${size}`}
className={buildClassName(styles.animatedIcon, centerAnimationClassNames)}
size={roundToNearestEven(size * CENTER_ICON_MULTIPLIER)}
tgsUrl={mediaDataCenterIcon}
play={isIntersecting && !shouldPause}
noLoop={!shouldLoop}
forceAlways
onLoad={markAnimationLoaded}
onEnded={unmarkAnimationLoaded}
/>
)}
{shouldRenderEffect && (
<> <>
<AnimatedSticker <AnimatedSticker
key={effectId} key={`${effectId}-${effectSize}`}
className={buildClassName(styles.effect, animationClassNames)} className={buildClassName(styles.effect, animationClassNames)}
size={effectSize} size={effectSize}
tgsUrl={mediaDataEffect} tgsUrl={mediaDataEffect}
@ -165,18 +193,12 @@ const ReactionAnimatedEmoji = ({
forceAlways forceAlways
onEnded={handleEnded} onEnded={handleEnded}
/> />
{isCustom && !assignedEffectId && isIntersecting && <CustomEmojiEffect reaction={reaction} />} {isCustom && !assignedEffectId && isIntersecting && (
{!isCustom && !withEffectOnly && ( <CustomEmojiEffect
<AnimatedSticker reaction={reaction}
key={centerIconId} className={animationClassNames}
className={buildClassName(styles.animatedIcon, animationClassNames)} particleSize={Math.max(size * CUSTOM_EMOJI_EFFECT_MULTIPLIER, MIN_PARTICLE_SIZE)}
size={roundToNearestEven(size * CENTER_ICON_MULTIPLIER)} onEnded={handleEnded}
tgsUrl={mediaDataCenterIcon}
play={isIntersecting}
noLoop
forceAlways
onLoad={markAnimationLoaded}
onEnded={unmarkAnimationLoaded}
/> />
)} )}
</> </>

View File

@ -7,6 +7,7 @@ import type {
ApiFormattedText, ApiFormattedText,
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer,
ApiTopic, ApiTopic,
ApiTypingStatus, ApiTypingStatus,
ApiUser, ApiUser,
@ -86,7 +87,7 @@ type StateProps = {
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
actionTargetChatId?: string; actionTargetChatId?: string;
lastMessageSender?: ApiUser | ApiChat; lastMessageSender?: ApiPeer;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
draft?: ApiFormattedText; draft?: ApiFormattedText;
isSelected?: boolean; isSelected?: boolean;
@ -264,10 +265,10 @@ const Chat: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
peer={peer} peer={peer}
isSavedMessages={user?.isSelf} isSavedMessages={user?.isSelf}
withStory={user && !user?.isSelf} withStory={!user?.isSelf}
withStoryGap={isAvatarOnlineShown} withStoryGap={isAvatarOnlineShown}
storyViewerOrigin={StoryViewerOrigin.ChatList} storyViewerOrigin={StoryViewerOrigin.ChatList}
storyViewerMode="single-user" storyViewerMode="single-peer"
/> />
<div className="avatar-badge-wrapper"> <div className="avatar-badge-wrapper">
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} /> <div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} />
@ -328,7 +329,7 @@ const Chat: FC<OwnProps & StateProps> = ({
isOpen={isReportModalOpen} isOpen={isReportModalOpen}
onClose={closeReportModal} onClose={closeReportModal}
onCloseAnimationEnd={unmarkRenderReportModal} onCloseAnimationEnd={unmarkRenderReportModal}
chatId={chatId} peerId={chatId}
subject="peer" subject="peer"
/> />
)} )}

View File

@ -355,7 +355,7 @@ export default memo(withGlobal<OwnProps>(
}, },
}, },
stories: { stories: {
orderedUserIds: { orderedPeerIds: {
archived: archivedStories, archived: archivedStories,
}, },
}, },

View File

@ -23,7 +23,7 @@ import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { IS_APP, IS_MAC_OS } from '../../../util/windowEnvironment'; import { IS_APP, IS_MAC_OS } from '../../../util/windowEnvironment';
import useUserStoriesPolling from '../../../hooks/polling/useUserStoriesPolling'; import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll'; import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
import useDebouncedCallback from '../../../hooks/useDebouncedCallback'; import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
@ -89,7 +89,7 @@ const ChatList: FC<OwnProps> = ({
const shouldDisplayArchive = isAllFolder && canDisplayArchive; const shouldDisplayArchive = isAllFolder && canDisplayArchive;
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId); const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
useUserStoriesPolling(orderedIds); usePeerStoriesPolling(orderedIds);
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX; const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
const archiveHeight = shouldDisplayArchive const archiveHeight = shouldDisplayArchive

View File

@ -4,8 +4,7 @@ import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiFormattedText, ApiMessage, ApiMessageOutgoingStatus, ApiChat, ApiFormattedText, ApiMessage, ApiMessageOutgoingStatus,
ApiTopic, ApiTypingStatus, ApiPeer, ApiTopic, ApiTypingStatus,
ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
@ -61,7 +60,7 @@ type StateProps = {
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
lastMessageSender?: ApiUser | ApiChat; lastMessageSender?: ApiPeer;
actionTargetChatId?: string; actionTargetChatId?: string;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
draft?: ApiFormattedText; draft?: ApiFormattedText;

View File

@ -4,7 +4,7 @@ import React, {
import { getGlobal } from '../../../../global'; import { getGlobal } from '../../../../global';
import type { import type {
ApiChat, ApiMessage, ApiTopic, ApiTypingStatus, ApiUser, ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser,
} from '../../../../api/types'; } from '../../../../api/types';
import type { Thread } from '../../../../global/types'; import type { Thread } from '../../../../global/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
@ -64,7 +64,7 @@ export default function useChatListEntry({
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
lastMessageTopic?: ApiTopic; lastMessageTopic?: ApiTopic;
lastMessageSender?: ApiUser | ApiChat; lastMessageSender?: ApiPeer;
actionTargetChatId?: string; actionTargetChatId?: string;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
isTopic?: boolean; isTopic?: boolean;

View File

@ -96,7 +96,13 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
storyViewerOrigin={StoryViewerOrigin.SearchResult} storyViewerOrigin={StoryViewerOrigin.SearchResult}
/> />
) : ( ) : (
<GroupChatInfo chatId={chatId} withUsername={withUsername} avatarSize="large" /> <GroupChatInfo
chatId={chatId}
withUsername={withUsername}
avatarSize="large"
withStory
storyViewerOrigin={StoryViewerOrigin.SearchResult}
/>
)} )}
{shouldRenderMuteModal && ( {shouldRenderMuteModal && (
<MuteChatModal <MuteChatModal

View File

@ -22,7 +22,7 @@ export function getSenderName(
const chat = chatsById[message.chatId]; const chat = chatsById[message.chatId];
if (chat) { if (chat) {
if (isUserId(senderId) && (sender as ApiUser).isSelf) { if ('isSelf' in sender && sender.isSelf) {
senderName = `${lang('FromYou')}${getChatTitle(lang, chat)}`; senderName = `${lang('FromYou')}${getChatTitle(lang, chat)}`;
} else if (isChatGroup(chat)) { } else if (isChatGroup(chat)) {
senderName += `${getChatTitle(lang, chat)}`; senderName += `${getChatTitle(lang, chat)}`;

View File

@ -41,12 +41,12 @@
align-items: flex-end; align-items: flex-end;
justify-content: flex-end; justify-content: flex-end;
margin-top: 1rem; margin-top: 1rem;
gap: 1rem;
} }
.subscribe-button { .x2 {
margin-left: 1rem !important; font-size: 1.25rem;
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); margin-inline-start: 0.25rem;
background: var(--premium-gradient);
} }
.button-icon { .button-icon {

View File

@ -15,6 +15,7 @@ import renderText from '../../../common/helpers/renderText';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang'; import useLang from '../../../../hooks/useLang';
import Icon from '../../../common/Icon';
import Button from '../../../ui/Button'; import Button from '../../../ui/Button';
import Modal from '../../../ui/Modal'; import Modal from '../../../ui/Modal';
import PremiumLimitsCompare from './PremiumLimitsCompare'; import PremiumLimitsCompare from './PremiumLimitsCompare';
@ -197,12 +198,13 @@ const PremiumLimitReachedModal: FC<OwnProps & StateProps> = ({
{canUpgrade {canUpgrade
&& ( && (
<Button <Button
className={buildClassName('confirm-dialog-button', styles.subscribeButton)} className="confirm-dialog-button"
isShiny isText
onClick={handleClick} onClick={handleClick}
color="primary" color="primary"
> >
{lang('IncreaseLimit')} {lang('IncreaseLimit')}
<Icon name="double-badge" className={styles.x2} />
</Button> </Button>
)} )}
</div> </div>

View File

@ -5,7 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiMessage, ApiPhoto, ApiUser, ApiMessage, ApiPeer, ApiPhoto, ApiUser,
} from '../../api/types'; } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
@ -63,7 +63,7 @@ type StateProps = {
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
canUpdateMedia?: boolean; canUpdateMedia?: boolean;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
avatarOwner?: ApiChat | ApiUser; avatarOwner?: ApiPeer;
avatarOwnerFallbackPhoto?: ApiPhoto; avatarOwnerFallbackPhoto?: ApiPhoto;
message?: ApiMessage; message?: ApiMessage;
chatMessages?: Record<number, ApiMessage>; chatMessages?: Record<number, ApiMessage>;
@ -205,7 +205,7 @@ const MediaViewer: FC<StateProps> = ({
const prevIsHidden = usePrevious<boolean | undefined>(isHidden); const prevIsHidden = usePrevious<boolean | undefined>(isHidden);
const prevOrigin = usePrevious(origin); const prevOrigin = usePrevious(origin);
const prevMediaId = usePrevious(mediaId); const prevMediaId = usePrevious(mediaId);
const prevAvatarOwner = usePrevious<ApiChat | ApiUser | undefined>(avatarOwner); const prevAvatarOwner = usePrevious<ApiPeer | undefined>(avatarOwner);
const prevBestImageData = usePrevious(bestImageData); const prevBestImageData = usePrevious(bestImageData);
const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined; const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined;
const hasFooter = Boolean(textParts); const hasFooter = Boolean(textParts);
@ -362,7 +362,7 @@ const MediaViewer: FC<StateProps> = ({
onClose={closeReportModal} onClose={closeReportModal}
subject="media" subject="media"
photo={avatarPhoto} photo={avatarPhoto}
chatId={avatarOwner?.id} peerId={avatarOwner?.id}
/> />
</div> </div>
<MediaViewerSlides <MediaViewerSlides

View File

@ -3,7 +3,7 @@ import React, { memo, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiMessage, ApiPhoto, ApiUser, ApiMessage, ApiPeer, ApiPhoto,
} from '../../api/types'; } from '../../api/types';
import type { MessageListType } from '../../global/types'; import type { MessageListType } from '../../global/types';
import type { MenuItemProps } from '../ui/MenuItem'; import type { MenuItemProps } from '../ui/MenuItem';
@ -50,7 +50,7 @@ type OwnProps = {
canUpdateMedia?: boolean; canUpdateMedia?: boolean;
isSingleMedia?: boolean; isSingleMedia?: boolean;
avatarPhoto?: ApiPhoto; avatarPhoto?: ApiPhoto;
avatarOwner?: ApiChat | ApiUser; avatarOwner?: ApiPeer;
fileName?: string; fileName?: string;
canReport?: boolean; canReport?: boolean;
selectMedia: (mediaId?: number) => void; selectMedia: (mediaId?: number) => void;

View File

@ -3,7 +3,7 @@ import React, { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global'; import { withGlobal } from '../../global';
import type { import type {
ApiChat, ApiDimensions, ApiMessage, ApiUser, ApiDimensions, ApiMessage, ApiPeer,
} from '../../api/types'; } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
@ -46,7 +46,7 @@ type StateProps = {
mediaId?: number; mediaId?: number;
senderId?: string; senderId?: string;
threadId?: number; threadId?: number;
avatarOwner?: ApiChat | ApiUser; avatarOwner?: ApiPeer;
message?: ApiMessage; message?: ApiMessage;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isProtected?: boolean; isProtected?: boolean;

View File

@ -2,14 +2,13 @@ import type { FC } from '../../lib/teact/teact';
import React from '../../lib/teact/teact'; import React from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChat, ApiMessage, ApiUser } from '../../api/types'; import type { ApiMessage, ApiPeer } from '../../api/types';
import { getSenderTitle, isUserId } from '../../global/helpers'; import { getSenderTitle } from '../../global/helpers';
import { import {
selectChat,
selectChatMessage, selectChatMessage,
selectPeer,
selectSender, selectSender,
selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import { formatMediaDateTime } from '../../util/dateFormat'; import { formatMediaDateTime } from '../../util/dateFormat';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
@ -30,7 +29,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
sender?: ApiUser | ApiChat; sender?: ApiPeer;
message?: ApiMessage; message?: ApiMessage;
}; };
@ -96,7 +95,7 @@ export default withGlobal<OwnProps>(
(global, { chatId, messageId, isAvatar }): StateProps => { (global, { chatId, messageId, isAvatar }): StateProps => {
if (isAvatar && chatId) { if (isAvatar && chatId) {
return { return {
sender: isUserId(chatId) ? selectUser(global, chatId) : selectChat(global, chatId), sender: selectPeer(global, chatId),
}; };
} }

View File

@ -1,21 +1,20 @@
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import type { ApiDimensions, ApiMessage } from '../../../api/types'; import type { ApiDimensions, ApiMessage } from '../../../api/types';
import { MediaViewerOrigin } from '../../../types'; import { MediaViewerOrigin } from '../../../types';
import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { getMessageHtmlId } from '../../../global/helpers';
import { applyStyles } from '../../../util/animation';
import { isElementInViewport } from '../../../util/isElementInViewport';
import stopEvent from '../../../util/stopEvent';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import windowSize from '../../../util/windowSize';
import { import {
calculateDimensions, calculateDimensions,
getMediaViewerAvailableDimensions, getMediaViewerAvailableDimensions,
MEDIA_VIEWER_MEDIA_QUERY, MEDIA_VIEWER_MEDIA_QUERY,
REM, REM,
} from '../../common/helpers/mediaDimensions'; } from '../../common/helpers/mediaDimensions';
import windowSize from '../../../util/windowSize';
import stopEvent from '../../../util/stopEvent';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { getMessageHtmlId } from '../../../global/helpers';
import { isElementInViewport } from '../../../util/isElementInViewport';
import { applyStyles } from '../../../util/animation';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;

View File

@ -1,7 +1,7 @@
import { useMemo } from '../../../lib/teact/teact'; import { useMemo } from '../../../lib/teact/teact';
import type { import type {
ApiChat, ApiMessage, ApiUser, ApiMessage, ApiPeer,
} from '../../../api/types'; } from '../../../api/types';
import { ApiMediaFormat } from '../../../api/types'; import { ApiMediaFormat } from '../../../api/types';
import { MediaViewerOrigin } from '../../../types'; import { MediaViewerOrigin } from '../../../types';
@ -34,7 +34,7 @@ import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
type UseMediaProps = { type UseMediaProps = {
mediaId?: number; mediaId?: number;
message?: ApiMessage; message?: ApiMessage;
avatarOwner?: ApiChat | ApiUser; avatarOwner?: ApiPeer;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
delay: number | false; delay: number | false;
}; };

View File

@ -3,7 +3,7 @@ import React, { useMemo, useRef } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiAudio, ApiChat, ApiMessage, ApiUser, ApiAudio, ApiChat, ApiMessage, ApiPeer,
} from '../../api/types'; } from '../../api/types';
import type { AudioOrigin } from '../../types'; import type { AudioOrigin } from '../../types';
@ -42,7 +42,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
sender?: ApiChat | ApiUser; sender?: ApiPeer;
chat?: ApiChat; chat?: ApiChat;
volume: number; volume: number;
playbackRate: number; playbackRate: number;

View File

@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../global';
import type { ApiChat, ApiChatSettings, ApiUser } from '../../api/types'; import type { ApiChat, ApiChatSettings, ApiUser } from '../../api/types';
import { import {
getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup, isUserId, getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup,
} from '../../global/helpers'; } from '../../global/helpers';
import { selectChat, selectUser } from '../../global/selectors'; import { selectChat, selectUser } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -174,6 +174,6 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => ({ (global, { chatId }): StateProps => ({
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
chat: selectChat(global, chatId), chat: selectChat(global, chatId),
user: isUserId(chatId) ? selectUser(global, chatId) : undefined, user: selectUser(global, chatId),
}), }),
)(ChatReportPanel)); )(ChatReportPanel));

View File

@ -630,7 +630,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
isOpen={isReportModalOpen} isOpen={isReportModalOpen}
onClose={closeReportModal} onClose={closeReportModal}
subject="peer" subject="peer"
chatId={chat.id} peerId={chat.id}
/> />
)} )}
</div> </div>

View File

@ -173,7 +173,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
}) => { }) => {
const { const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
loadMessageViews, loadUserStoriesByIds, loadMessageViews, loadPeerStoriesByIds,
} = getActions(); } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -267,17 +267,17 @@ const MessageList: FC<OwnProps & StateProps> = ({
if (!storyDataList.length) return; if (!storyDataList.length) return;
const storiesByUserIds = storyDataList.reduce((acc, storyData) => { const storiesByPeerIds = storyDataList.reduce((acc, storyData) => {
const { userId, id } = storyData!; const { peerId, id } = storyData!;
if (!acc[userId]) { if (!acc[peerId]) {
acc[userId] = []; acc[peerId] = [];
} }
acc[userId].push(id); acc[peerId].push(id);
return acc; return acc;
}, {} as Record<string, number[]>); }, {} as Record<string, number[]>);
Object.entries(storiesByUserIds).forEach(([userId, storyIds]) => { Object.entries(storiesByPeerIds).forEach(([peerId, storyIds]) => {
loadUserStoriesByIds({ userId, storyIds }); loadPeerStoriesByIds({ peerId, storyIds });
}); });
}, MESSAGE_STORY_POLLING_INTERVAL); }, MESSAGE_STORY_POLLING_INTERVAL);

View File

@ -5,7 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiMessage, ApiTypingStatus, ApiUser, ApiChat, ApiMessage, ApiPeer, ApiTypingStatus,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState, MessageListType } from '../../global/types'; import type { GlobalState, MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals'; import type { Signal } from '../../util/signals';
@ -98,7 +98,7 @@ type StateProps = {
pinnedMessageIds?: number[] | number; pinnedMessageIds?: number[] | number;
messagesById?: Record<number, ApiMessage>; messagesById?: Record<number, ApiMessage>;
canUnpin?: boolean; canUnpin?: boolean;
topMessageSender?: ApiChat | ApiUser; topMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
isLeftColumnShown?: boolean; isLeftColumnShown?: boolean;
@ -401,6 +401,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
withMediaViewer={threadId === MAIN_THREAD_ID} withMediaViewer={threadId === MAIN_THREAD_ID}
withFullInfo={threadId === MAIN_THREAD_ID} withFullInfo={threadId === MAIN_THREAD_ID}
withUpdatingStatus withUpdatingStatus
withStory
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
noRtl noRtl
/> />
)} )}

View File

@ -4,12 +4,11 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; import type { ApiMessage, ApiPeer } from '../../../api/types';
import { isUserId, stripCustomEmoji } from '../../../global/helpers'; import { stripCustomEmoji } from '../../../global/helpers';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectChat,
selectChatMessage, selectChatMessage,
selectCurrentMessageList, selectCurrentMessageList,
selectEditingId, selectEditingId,
@ -18,10 +17,10 @@ import {
selectForwardedSender, selectForwardedSender,
selectIsChatWithSelf, selectIsChatWithSelf,
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectPeer,
selectReplyingToId, selectReplyingToId,
selectSender, selectSender,
selectTabState, selectTabState,
selectUser,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
@ -45,7 +44,7 @@ type StateProps = {
replyingToId?: number; replyingToId?: number;
editingId?: number; editingId?: number;
message?: ApiMessage; message?: ApiMessage;
sender?: ApiUser | ApiChat; sender?: ApiPeer;
shouldAnimate?: boolean; shouldAnimate?: boolean;
forwardedMessagesCount?: number; forwardedMessagesCount?: number;
noAuthors?: boolean; noAuthors?: boolean;
@ -313,7 +312,7 @@ export default memo(withGlobal<OwnProps>(
message = forwardedMessages?.[0]; message = forwardedMessages?.[0];
} }
let sender: ApiChat | ApiUser | undefined; let sender: ApiPeer | undefined;
if (replyingToId && message && !shouldForceShowEditing) { if (replyingToId && message && !shouldForceShowEditing) {
const { forwardInfo } = message; const { forwardInfo } = message;
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
@ -332,7 +331,7 @@ export default memo(withGlobal<OwnProps>(
} }
} }
if (!sender) { if (!sender) {
sender = isUserId(fromChatId!) ? selectUser(global, fromChatId!) : selectChat(global, fromChatId!); sender = selectPeer(global, fromChatId!);
} }
} }

View File

@ -16,6 +16,8 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import MediaAreaOverlay from '../../story/mediaArea/MediaAreaOverlay';
import styles from './BaseStory.module.scss'; import styles from './BaseStory.module.scss';
interface OwnProps { interface OwnProps {
@ -28,7 +30,7 @@ interface OwnProps {
function BaseStory({ function BaseStory({
story, isPreview, isProtected, isConnected, story, isPreview, isProtected, isConnected,
}: OwnProps) { }: OwnProps) {
const { openStoryViewer, loadUserStoriesByIds, showNotification } = getActions(); const { openStoryViewer, loadPeerStoriesByIds, showNotification } = getActions();
const lang = useLang(); const lang = useLang();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
@ -56,7 +58,7 @@ function BaseStory({
useEffect(() => { useEffect(() => {
if (story && !(isLoaded || isExpired)) { if (story && !(isLoaded || isExpired)) {
loadUserStoriesByIds({ userId: story.userId, storyIds: [story.id] }); loadPeerStoriesByIds({ peerId: story.peerId, storyIds: [story.id] });
} }
}, [story, isExpired, isLoaded]); }, [story, isExpired, isLoaded]);
@ -69,9 +71,9 @@ function BaseStory({
} }
openStoryViewer({ openStoryViewer({
userId: story!.userId, peerId: story!.peerId,
storyId: story!.id, storyId: story!.id,
isSingleUser: true, isSinglePeer: true,
isSingleStory: true, isSingleStory: true,
}); });
}); });
@ -83,12 +85,15 @@ function BaseStory({
> >
{!isExpired && isPreview && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />} {!isExpired && isPreview && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />}
{shouldRender && ( {shouldRender && (
<img <>
src={mediaUrl} <img
alt="" src={mediaUrl}
className={buildClassName(styles.media, isPreview && styles.linkPreview, transitionClassNames)} alt=""
draggable={false} className={buildClassName(styles.media, isPreview && styles.linkPreview, transitionClassNames)}
/> draggable={false}
/>
{isLoaded && <MediaAreaOverlay story={story} className={transitionClassNames} />}
</>
)} )}
{isExpired && ( {isExpired && (
<span> <span>

View File

@ -6,7 +6,7 @@ import type {
ApiThreadInfo, ApiThreadInfo,
} from '../../../api/types'; } from '../../../api/types';
import { isUserId } from '../../../global/helpers'; import { selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatIntegerCompact } from '../../../util/textFormat'; import { formatIntegerCompact } from '../../../util/textFormat';
@ -44,10 +44,10 @@ const CommentButton: FC<OwnProps> = ({
} }
// No need for expensive global updates on chats and users, so we avoid them // No need for expensive global updates on chats and users, so we avoid them
const { users: { byId: usersById }, chats: { byId: chatsById } } = getGlobal(); const global = getGlobal();
return recentReplierIds.map((peerId) => { return recentReplierIds.map((peerId) => {
return isUserId(peerId) ? usersById[peerId] : chatsById[peerId]; return selectPeer(global, peerId);
}).filter(Boolean); }).filter(Boolean);
}, [recentReplierIds]); }, [recentReplierIds]);

View File

@ -4,7 +4,7 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; import type { ApiMessage, ApiPeer } from '../../../api/types';
import type { ISettings } from '../../../types'; import type { ISettings } from '../../../types';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
@ -47,7 +47,7 @@ const DEFAULT_MAP_CONFIG = {
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
peer?: ApiUser | ApiChat; peer?: ApiPeer;
isInSelectMode?: boolean; isInSelectMode?: boolean;
isSelected?: boolean; isSelected?: boolean;
theme: ISettings['theme']; theme: ISettings['theme'];

View File

@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact'; import React from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types'; import type { ApiPeer } from '../../../api/types';
import { ApiMessageEntityTypes } from '../../../api/types'; import { ApiMessageEntityTypes } from '../../../api/types';
import { selectUser } from '../../../global/selectors'; import { selectUser } from '../../../global/selectors';
@ -14,7 +14,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
userOrChat?: ApiUser | ApiChat; userOrChat?: ApiPeer;
}; };
const MentionLink: FC<OwnProps & StateProps> = ({ const MentionLink: FC<OwnProps & StateProps> = ({

View File

@ -6,10 +6,10 @@ import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiAvailableReaction, ApiAvailableReaction,
ApiChat,
ApiChatMember, ApiChatMember,
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer,
ApiReaction, ApiReaction,
ApiThreadInfo, ApiThreadInfo,
ApiTopic, ApiTopic,
@ -75,6 +75,7 @@ import {
selectIsMessageSelected, selectIsMessageSelected,
selectMessageIdsByGroupId, selectMessageIdsByGroupId,
selectOutgoingStatus, selectOutgoingStatus,
selectPeerStory,
selectPerformanceSettingsValue, selectPerformanceSettingsValue,
selectReplySender, selectReplySender,
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
@ -89,7 +90,6 @@ import {
selectTopicFromMessage, selectTopicFromMessage,
selectUploadProgress, selectUploadProgress,
selectUser, selectUser,
selectUserStory,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { isAnimatingScroll } from '../../../util/animateScroll'; import { isAnimatingScroll } from '../../../util/animateScroll';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -199,14 +199,14 @@ type OwnProps =
type StateProps = { type StateProps = {
theme: ISettings['theme']; theme: ISettings['theme'];
forceSenderName?: boolean; forceSenderName?: boolean;
sender?: ApiUser | ApiChat; sender?: ApiPeer;
canShowSender: boolean; canShowSender: boolean;
originSender?: ApiUser | ApiChat; originSender?: ApiPeer;
botSender?: ApiUser; botSender?: ApiUser;
isThreadTop?: boolean; isThreadTop?: boolean;
shouldHideReply?: boolean; shouldHideReply?: boolean;
replyMessage?: ApiMessage; replyMessage?: ApiMessage;
replyMessageSender?: ApiUser | ApiChat; replyMessageSender?: ApiPeer;
replyStory?: ApiTypeStory; replyStory?: ApiTypeStory;
storySender?: ApiUser; storySender?: ApiUser;
outgoingStatus?: ApiMessageOutgoingStatus; outgoingStatus?: ApiMessageOutgoingStatus;
@ -499,7 +499,7 @@ const Message: FC<OwnProps & StateProps> = ({
const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender); const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender);
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender; const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
const senderPeer = forwardInfo ? originSender : messageSender; const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
const { const {
handleMouseDown, handleMouseDown,
@ -1440,7 +1440,7 @@ export default memo(withGlobal<OwnProps>(
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
const webPageStoryData = message.content.webPage?.story; const webPageStoryData = message.content.webPage?.story;
const webPageStory = webPageStoryData const webPageStory = webPageStoryData
? selectUserStory(global, webPageStoryData.userId, webPageStoryData.id) ? selectPeerStory(global, webPageStoryData.peerId, webPageStoryData.id)
: undefined; : undefined;
const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id); const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id);
@ -1463,7 +1463,7 @@ export default memo(withGlobal<OwnProps>(
const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo)); const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo));
const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate'; const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate';
const replyStory = replyToStoryId && replyToStoryUserId const replyStory = replyToStoryId && replyToStoryUserId
? selectUserStory(global, replyToStoryUserId, replyToStoryId) ? selectPeerStory(global, replyToStoryUserId, replyToStoryId)
: undefined; : undefined;
const storySender = replyToStoryUserId ? selectUser(global, replyToStoryUserId) : undefined; const storySender = replyToStoryUserId ? selectUser(global, replyToStoryUserId) : undefined;

View File

@ -9,6 +9,7 @@ import type {
ApiChat, ApiChat,
ApiChatReactions, ApiChatReactions,
ApiMessage, ApiMessage,
ApiPeer,
ApiReaction, ApiReaction,
ApiSponsoredMessage, ApiSponsoredMessage,
ApiStickerSet, ApiStickerSet,
@ -78,7 +79,7 @@ type OwnProps = {
canClosePoll?: boolean; canClosePoll?: boolean;
isDownloading?: boolean; isDownloading?: boolean;
canShowSeenBy?: boolean; canShowSeenBy?: boolean;
seenByRecentPeers?: (ApiChat | ApiUser)[]; seenByRecentPeers?: ApiPeer[];
noReplies?: boolean; noReplies?: boolean;
hasCustomEmoji?: boolean; hasCustomEmoji?: boolean;
customEmojiSets?: ApiStickerSet[]; customEmojiSets?: ApiStickerSet[];

View File

@ -10,7 +10,7 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiMessage, ApiPoll, ApiPollAnswer, ApiUser, ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer,
} from '../../../api/types'; } from '../../../api/types';
import type { LangFn } from '../../../hooks/useLang'; import type { LangFn } from '../../../hooks/useLang';
@ -137,7 +137,7 @@ const Poll: FC<OwnProps & StateProps> = ({
// No need for expensive global updates on chats or users, so we avoid them // No need for expensive global updates on chats or users, so we avoid them
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
return recentVoterIds ? recentVoterIds.reduce((result: (ApiChat | ApiUser)[], id) => { return recentVoterIds ? recentVoterIds.reduce((result: ApiPeer[], id) => {
const chat = chatsById[id]; const chat = chatsById[id];
const user = usersById[id]; const user = usersById[id];
if (user) { if (user) {

View File

@ -3,7 +3,7 @@ import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global'; import { getActions, getGlobal } from '../../../global';
import type { import type {
ApiChat, ApiMessage, ApiReactionCount, ApiUser, ApiMessage, ApiPeer, ApiReactionCount,
} from '../../../api/types'; } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
@ -49,7 +49,7 @@ const ReactionButton: FC<{
return recentReactions return recentReactions
.filter((recentReaction) => isSameReaction(recentReaction.reaction, reaction.reaction)) .filter((recentReaction) => isSameReaction(recentReaction.reaction, reaction.reaction))
.map((recentReaction) => usersById[recentReaction.peerId] || chatsById[recentReaction.peerId]) .map((recentReaction) => usersById[recentReaction.peerId] || chatsById[recentReaction.peerId])
.filter(Boolean) as (ApiChat | ApiUser)[]; .filter(Boolean) as ApiPeer[];
}, [reaction.reaction, recentReactions, withRecentReactors]); }, [reaction.reaction, recentReactions, withRecentReactors]);
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {

View File

@ -8,10 +8,10 @@ import type {
} from '../../../api/types'; } from '../../../api/types';
import type { IAnchorPosition } from '../../../types'; import type { IAnchorPosition } from '../../../types';
import { isUserId } from '../../../global/helpers'; import { getStoryKey, isUserId } from '../../../global/helpers';
import { import {
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium, selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium,
selectTabState, selectUserStory, selectPeerStory, selectTabState,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import parseMessageInput from '../../../util/parseMessageInput'; import parseMessageInput from '../../../util/parseMessageInput';
@ -66,7 +66,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
const renderedMessageId = useCurrentOrPrev(message?.id, true); const renderedMessageId = useCurrentOrPrev(message?.id, true);
const renderedChatId = useCurrentOrPrev(message?.chatId, true); const renderedChatId = useCurrentOrPrev(message?.chatId, true);
const renderedStoryUserId = useCurrentOrPrev(story?.userId, true); const renderedStoryPeerId = useCurrentOrPrev(story?.peerId, true);
const renderedStoryId = useCurrentOrPrev(story?.id); const renderedStoryId = useCurrentOrPrev(story?.id);
const storedPosition = useCurrentOrPrev(position, true); const storedPosition = useCurrentOrPrev(position, true);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -89,7 +89,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
const getMenuElement = useLastCallback(() => menuRef.current); const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ const getLayout = useLastCallback(() => ({
withPortal: true, withPortal: true,
isDense: !renderedStoryUserId, isDense: !renderedStoryPeerId,
deltaX: !getIsMobile() && menuRef.current deltaX: !getIsMobile() && menuRef.current
? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2 ? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2
: 0, : 0,
@ -146,7 +146,11 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
if (!sendAsMessage) { if (!sendAsMessage) {
sendStoryReaction({ sendStoryReaction({
userId: renderedStoryUserId!, storyId: renderedStoryId!, reaction, shouldAddToRecent: true, peerId: renderedStoryPeerId!,
storyId: renderedStoryId!,
containerId: getStoryKey(renderedStoryPeerId!, renderedStoryId!),
reaction,
shouldAddToRecent: true,
}); });
closeReactionPicker(); closeReactionPicker();
return; return;
@ -223,15 +227,15 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((global): StateProps => { export default memo(withGlobal<OwnProps>((global): StateProps => {
const state = selectTabState(global); const state = selectTabState(global);
const { const {
chatId, messageId, storyUserId, storyId, position, sendAsMessage, chatId, messageId, storyPeerId, storyId, position, sendAsMessage,
} = state.reactionPicker || {}; } = state.reactionPicker || {};
const story = storyUserId && storyId const story = storyPeerId && storyId
? selectUserStory(global, storyUserId, storyId) as ApiStory | ApiStorySkipped ? selectPeerStory(global, storyPeerId, storyId) as ApiStory | ApiStorySkipped
: undefined; : undefined;
const chat = chatId ? selectChat(global, chatId) : undefined; const chat = chatId ? selectChat(global, chatId) : undefined;
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined; const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
const isPrivateChat = chatId ? isUserId(chatId) : Boolean(storyUserId); const isPrivateChat = isUserId(chatId || storyPeerId || '');
const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some'; const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some';
const areCustomReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all' const areCustomReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all'
&& chatFullInfo?.enabledReactions?.areCustomAllowed; && chatFullInfo?.enabledReactions?.areCustomAllowed;

View File

@ -5,7 +5,7 @@ import type {
ApiMessage, ApiTypeStory, ApiMessage, ApiTypeStory,
} from '../../../api/types'; } from '../../../api/types';
import { selectUserStory } from '../../../global/selectors'; import { selectPeerStory } from '../../../global/selectors';
import BaseStory from './BaseStory'; import BaseStory from './BaseStory';
@ -34,10 +34,10 @@ function Story({
} }
export default memo(withGlobal<OwnProps>((global, { message }): StateProps => { export default memo(withGlobal<OwnProps>((global, { message }): StateProps => {
const { id, userId } = message.content.storyData!; const { id, peerId } = message.content.storyData!;
return { return {
story: selectUserStory(global, userId, id), story: selectPeerStory(global, peerId, id),
isConnected: global.connectionState === 'connectionStateReady', isConnected: global.connectionState === 'connectionStateReady',
}; };
})(Story)); })(Story));

View File

@ -2,12 +2,15 @@ import React, { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiMessage, ApiTypeStory, ApiUser, ApiMessage, ApiPeer, ApiTypeStory, ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import { getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers'; import { getSenderTitle, getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers';
import { import {
selectUser, selectUserStories, selectUserStory, selectPeer,
selectPeerStories,
selectPeerStory,
selectUser,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -23,13 +26,13 @@ interface OwnProps {
interface StateProps { interface StateProps {
story?: ApiTypeStory; story?: ApiTypeStory;
user?: ApiUser; peer?: ApiPeer;
targetUser?: ApiUser; targetUser?: ApiUser;
isUnread?: boolean; isUnread?: boolean;
} }
function StoryMention({ function StoryMention({
message, story, user, isUnread, targetUser, message, story, peer, isUnread, targetUser,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { openStoryViewer } = getActions(); const { openStoryViewer } = getActions();
@ -39,9 +42,9 @@ function StoryMention({
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
openStoryViewer({ openStoryViewer({
userId: story!.userId, peerId: story!.peerId,
storyId: story!.id, storyId: story!.id,
isSingleUser: true, isSinglePeer: true,
isSingleStory: true, isSingleStory: true,
}); });
}); });
@ -55,10 +58,10 @@ function StoryMention({
const imgBlobUrl = useMedia(imageHash); const imgBlobUrl = useMedia(imageHash);
const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri; const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri;
useEnsureStory(storyData!.userId, storyData!.id, story); useEnsureStory(storyData!.peerId, storyData!.id, story);
function getTitle() { function getTitle() {
if (user?.isSelf) { if (peer && 'isSelf' in peer && peer.isSelf) {
return isDeleted return isDeleted
? lang('ExpiredStoryMentioned', getUserFirstOrLastName(targetUser)) ? lang('ExpiredStoryMentioned', getUserFirstOrLastName(targetUser))
: lang('StoryYouMentionedTitle', getUserFirstOrLastName(targetUser)); : lang('StoryYouMentionedTitle', getUserFirstOrLastName(targetUser));
@ -66,7 +69,7 @@ function StoryMention({
return isDeleted return isDeleted
? lang('ExpiredStoryMention') ? lang('ExpiredStoryMention')
: lang('StoryMentionedTitle', getUserFirstOrLastName(user)); : lang('StoryMentionedTitle', getSenderTitle(lang, peer!));
} }
return ( return (
@ -90,12 +93,12 @@ function StoryMention({
} }
export default memo(withGlobal<OwnProps>((global, { message }): StateProps => { export default memo(withGlobal<OwnProps>((global, { message }): StateProps => {
const { id, userId } = message.content.storyData!; const { id, peerId } = message.content.storyData!;
const lastReadId = selectUserStories(global, userId)?.lastReadId; const lastReadId = selectPeerStories(global, peerId)?.lastReadId;
return { return {
story: selectUserStory(global, userId, id), story: selectPeerStory(global, peerId, id),
user: selectUser(global, userId), peer: selectPeer(global, peerId),
targetUser: selectUser(global, message.chatId), targetUser: selectUser(global, message.chatId),
isUnread: Boolean(lastReadId && lastReadId < id), isUnread: Boolean(lastReadId && lastReadId < id),
}; };

View File

@ -81,7 +81,7 @@ const WebPage: FC<OwnProps> = ({
const { story: storyData } = webPage || {}; const { story: storyData } = webPage || {};
useEnsureStory(storyData?.userId, storyData?.id, story); useEnsureStory(storyData?.peerId, storyData?.id, story);
if (!webPage) { if (!webPage) {
return undefined; return undefined;

View File

@ -2,8 +2,7 @@ import type React from '../../../../lib/teact/teact';
import { getActions } from '../../../../global'; import { getActions } from '../../../../global';
import type { import type {
ApiChat, ApiMessage, ApiStory, ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser,
ApiTopic, ApiUser,
} from '../../../../api/types'; } from '../../../../api/types';
import type { LangFn } from '../../../../hooks/useLang'; import type { LangFn } from '../../../../hooks/useLang';
import type { IAlbum } from '../../../../types'; import type { IAlbum } from '../../../../types';
@ -23,8 +22,8 @@ export default function useInnerHandlers(
isScheduled?: boolean, isScheduled?: boolean,
isChatWithRepliesBot?: boolean, isChatWithRepliesBot?: boolean,
album?: IAlbum, album?: IAlbum,
avatarPeer?: ApiUser | ApiChat, avatarPeer?: ApiPeer,
senderPeer?: ApiUser | ApiChat, senderPeer?: ApiPeer,
botSender?: ApiUser, botSender?: ApiUser,
messageTopic?: ApiTopic, messageTopic?: ApiTopic,
isTranslatingChat?: boolean, isTranslatingChat?: boolean,
@ -185,7 +184,7 @@ export default function useInnerHandlers(
const handleStoryClick = useLastCallback(() => { const handleStoryClick = useLastCallback(() => {
if (!story) return; if (!story) return;
openStoryViewer({ openStoryViewer({
userId: story.userId, peerId: story.peerId,
storyId: story.id, storyId: story.id,
isSingleStory: true, isSingleStory: true,
}); });

View File

@ -136,6 +136,7 @@
} }
.tos-checkbox { .tos-checkbox {
margin-left: 0.5rem;
padding-left: 4rem; padding-left: 4rem;
:global(.Checkbox-main) { :global(.Checkbox-main) {

View File

@ -71,7 +71,7 @@ const Checkout: FC<OwnProps> = ({
const isInteractive = Boolean(dispatch); const isInteractive = Boolean(dispatch);
const { const {
photo, title, text, isRecurring, recurringTermsUrl, suggestedTipAmounts, maxTipAmount, photo, title, text, termsUrl, suggestedTipAmounts, maxTipAmount,
} = invoice || {}; } = invoice || {};
const { const {
paymentMethod, paymentMethod,
@ -218,7 +218,7 @@ const Checkout: FC<OwnProps> = ({
icon: 'truck', icon: 'truck',
onClick: isInteractive ? handleShippingMethodClick : undefined, onClick: isInteractive ? handleShippingMethodClick : undefined,
})} })}
{isRecurring && renderTos(recurringTermsUrl!)} {termsUrl && renderTos(termsUrl)}
</div> </div>
</div> </div>
); );

View File

@ -35,17 +35,17 @@ import {
selectChatMessages, selectChatMessages,
selectCurrentMediaSearch, selectCurrentMediaSearch,
selectIsRightColumnShown, selectIsRightColumnShown,
selectPeerFullInfo,
selectPeerStories,
selectTabState, selectTabState,
selectTheme, selectTheme,
selectUser, selectUser,
selectUserFullInfo,
selectUserStories,
} from '../../global/selectors'; } from '../../global/selectors';
import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { getSenderName } from '../left/search/helpers/getSenderName'; import { getSenderName } from '../left/search/helpers/getSenderName';
import useUserStoriesPolling from '../../hooks/polling/useUserStoriesPolling'; import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
import useCacheBuster from '../../hooks/useCacheBuster'; import useCacheBuster from '../../hooks/useCacheBuster';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
@ -170,7 +170,7 @@ const Profile: FC<OwnProps & StateProps> = ({
focusMessage, focusMessage,
loadProfilePhotos, loadProfilePhotos,
setNewChatMembersDialogState, setNewChatMembersDialogState,
loadUserPinnedStories, loadPeerPinnedStories,
loadStoriesArchive, loadStoriesArchive,
} = getActions(); } = getActions();
@ -215,18 +215,18 @@ 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 handleLoadUserStories = useCallback(({ offsetId }: { offsetId: number }) => { const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
loadUserPinnedStories({ userId: chatId, offsetId }); loadPeerPinnedStories({ peerId: chatId, offsetId });
}, [chatId]); }, [chatId]);
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => { const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
loadStoriesArchive({ offsetId }); loadStoriesArchive({ peerId: currentUserId!, offsetId });
}, []); }, [currentUserId]);
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
loadMoreMembers, loadMoreMembers,
loadCommonChats, loadCommonChats,
searchMediaMessagesLocal, searchMediaMessagesLocal,
handleLoadUserStories, handleLoadPeerStories,
handleLoadStoriesArchive, handleLoadStoriesArchive,
tabType, tabType,
mediaSearchType, mediaSearchType,
@ -246,7 +246,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|| (!hasMembersTab && resultType === 'media'); || (!hasMembersTab && resultType === 'media');
const activeKey = tabs.findIndex(({ type }) => type === resultType); const activeKey = tabs.findIndex(({ type }) => type === resultType);
useUserStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined); usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
const { handleScroll } = useProfileState(containerRef, resultType, profileState, onProfileStateChange); const { handleScroll } = useProfileState(containerRef, resultType, profileState, onProfileStateChange);
@ -606,24 +606,22 @@ export default memo(withGlobal<OwnProps>(
const activeDownloads = selectActiveDownloads(global, chatId); const activeDownloads = selectActiveDownloads(global, chatId);
let hasCommonChatsTab; let hasCommonChatsTab;
let hasStoriesTab;
let resolvedUserId; let resolvedUserId;
let user; let user;
let storyIds;
let archiveStoryIds;
let storyByIds;
if (isUserId(chatId)) { if (isUserId(chatId)) {
resolvedUserId = chatId; resolvedUserId = chatId;
user = selectUser(global, resolvedUserId); user = selectUser(global, resolvedUserId);
const userFullInfo = selectUserFullInfo(global, chatId);
hasCommonChatsTab = user && !user.isSelf && !isUserBot(user); hasCommonChatsTab = user && !user.isSelf && !isUserBot(user);
hasStoriesTab = user && (user.isSelf || (!user.areStoriesHidden && userFullInfo?.hasPinnedStories));
const userStories = hasStoriesTab ? selectUserStories(global, user!.id) : undefined;
storyIds = userStories?.pinnedIds;
storyByIds = userStories?.byId;
archiveStoryIds = userStories?.archiveIds;
} }
const peer = user || chat;
const peerFullInfo = selectPeerFullInfo(global, chatId);
const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories));
const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined;
const storyIds = peerStories?.pinnedIds;
const storyByIds = peerStories?.byId;
const archiveStoryIds = peerStories?.archiveIds;
return { return {
theme: selectTheme(global), theme: selectTheme(global),
isChannel, isChannel,

View File

@ -5,7 +5,7 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChat, ApiMessage, ApiUser } from '../../api/types'; import type { ApiMessage, ApiPeer } from '../../api/types';
import { import {
selectChat, selectChat,
@ -130,7 +130,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
message, senderPeer, onClick, message, senderPeer, onClick,
}: { }: {
message: ApiMessage; message: ApiMessage;
senderPeer: ApiUser | ApiChat; senderPeer: ApiPeer;
onClick: NoneToVoidFunction; onClick: NoneToVoidFunction;
}) => { }) => {
const text = renderMessageSummary(lang, message, undefined, query); const text = renderMessageSummary(lang, message, undefined, query);

View File

@ -29,9 +29,9 @@ type OwnProps = {
selectedUserId?: string; selectedUserId?: string;
isPromotedByCurrentUser?: boolean; isPromotedByCurrentUser?: boolean;
isNewAdmin?: boolean; isNewAdmin?: boolean;
isActive: boolean;
onScreenSelect: (screen: ManagementScreens) => void; onScreenSelect: (screen: ManagementScreens) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
isActive: boolean;
}; };
type StateProps = { type StateProps = {
@ -49,10 +49,10 @@ type StateProps = {
const CUSTOM_TITLE_MAX_LENGTH = 16; const CUSTOM_TITLE_MAX_LENGTH = 16;
const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
isActive,
isNewAdmin, isNewAdmin,
selectedUserId, selectedUserId,
defaultRights, defaultRights,
onScreenSelect,
chat, chat,
usersById, usersById,
currentUserId, currentUserId,
@ -62,7 +62,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
isForum, isForum,
isFormFullyDisabled, isFormFullyDisabled,
onClose, onClose,
isActive, onScreenSelect,
}) => { }) => {
const { updateChatAdmin } = getActions(); const { updateChatAdmin } = getActions();
@ -260,6 +260,42 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
onChange={handlePermissionChange} onChange={handlePermissionChange}
/> />
</div> </div>
{isChannel && (
<div className="ListItem">
<Checkbox
name="postStories"
checked={Boolean(permissions.postStories)}
label={lang('EditAdminPostStories')}
blocking
disabled={getControlIsDisabled('postStories')}
onChange={handlePermissionChange}
/>
</div>
)}
{isChannel && (
<div className="ListItem">
<Checkbox
name="editStories"
checked={Boolean(permissions.editStories)}
label={lang('EditAdminEditStories')}
blocking
disabled={getControlIsDisabled('editStories')}
onChange={handlePermissionChange}
/>
</div>
)}
{isChannel && (
<div className="ListItem">
<Checkbox
name="deleteStories"
checked={Boolean(permissions.deleteStories)}
label={lang('EditAdminDeleteStories')}
blocking
disabled={getControlIsDisabled('deleteStories')}
onChange={handlePermissionChange}
/>
</div>
)}
{!isChannel && ( {!isChannel && (
<div className="ListItem"> <div className="ListItem">
<Checkbox <Checkbox

View File

@ -14,6 +14,7 @@ import {
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'; import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
@ -105,6 +106,8 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds; return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds;
}, [members, userStatusesById, noAdmins, adminIds]); }, [members, userStatusesById, noAdmins, adminIds]);
usePeerStoriesPolling(memberIds);
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
@ -223,7 +226,7 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
onClick={() => handleMemberClick(id)} onClick={() => handleMemberClick(id)}
contextActions={getMemberContextAction(id)} contextActions={getMemberContextAction(id)}
> >
<PrivateChatInfo userId={id} forceShowSelf /> <PrivateChatInfo userId={id} forceShowSelf withStory />
</ListItem> </ListItem>
))} ))}
</InfiniteScroll> </InfiniteScroll>

View File

@ -1,55 +0,0 @@
import React, { memo } from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { ApiMediaArea } from '../../api/types';
import type { IDimensions } from '../../global/types';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import styles from './StoryViewer.module.scss';
type OwnProps = {
mediaAreas?: ApiMediaArea[];
mediaDimensions: IDimensions;
};
const MediaAreaOverlay = ({ mediaAreas, mediaDimensions }: OwnProps) => {
const { openMapModal } = getActions();
const handleMediaAreaClick = (mediaArea: ApiMediaArea) => {
if (mediaArea.geo) {
openMapModal({ geoPoint: mediaArea.geo });
}
};
return (
<div
className={buildClassName(styles.mediaAreaOverlay, styles.media)}
style={buildStyle(`aspect-ratio: ${mediaDimensions.width} / ${mediaDimensions.height}`)}
>
{mediaAreas?.map((mediaArea) => (
<div
className={styles.mediaArea}
style={prepareStyle(mediaArea)}
onClick={() => handleMediaAreaClick(mediaArea)}
/>
))}
</div>
);
};
function prepareStyle(mediaArea: ApiMediaArea) {
const {
x, y, width, height, rotation,
} = mediaArea.coordinates;
return buildStyle(
`left: ${x}%`,
`top: ${y}%`,
`width: ${width}%`,
`height: ${height}%`,
`transform: rotate(${rotation}deg) translate(-50%, -50%)`,
);
}
export default memo(MediaAreaOverlay);

View File

@ -18,6 +18,7 @@ import useMenuPosition from '../../hooks/useMenuPosition';
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 styles from './MediaStory.module.scss'; import styles from './MediaStory.module.scss';
@ -30,7 +31,7 @@ interface OwnProps {
function MediaStory({ story, isProtected, isArchive }: OwnProps) { function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const { const {
openStoryViewer, openStoryViewer,
loadUserSkippedStories, loadPeerSkippedStories,
toggleStoryPinned, toggleStoryPinned,
showNotification, showNotification,
} = getActions(); } = getActions();
@ -44,6 +45,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const getMenuElement = useLastCallback(() => document.querySelector('#portals .story-context-menu .bubble')); const getMenuElement = useLastCallback(() => document.querySelector('#portals .story-context-menu .bubble'));
const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true })); const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true }));
const peerId = story && story.peerId;
const isFullyLoaded = story && 'content' in story; const isFullyLoaded = story && 'content' in story;
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;
@ -53,7 +55,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
useEffect(() => { useEffect(() => {
if (story && !(isFullyLoaded || isDeleted)) { if (story && !(isFullyLoaded || isDeleted)) {
loadUserSkippedStories({ userId: story.userId }); loadPeerSkippedStories({ peerId: story.peerId });
} }
}, [isDeleted, isFullyLoaded, story]); }, [isDeleted, isFullyLoaded, story]);
@ -74,13 +76,13 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
openStoryViewer({ openStoryViewer({
userId: story.userId, peerId: story.peerId,
storyId: story.id, storyId: story.id,
isSingleUser: true, isSinglePeer: true,
isPrivate: true, isPrivate: true,
isArchive, isArchive,
}); });
}, [isArchive, story.id, story.userId]); }, [isArchive, story.id, story.peerId]);
const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => { const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
preventMessageInputBlurWithBubbling(e); preventMessageInputBlurWithBubbling(e);
@ -90,7 +92,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const handlePinClick = useLastCallback((e: React.SyntheticEvent) => { const handlePinClick = useLastCallback((e: React.SyntheticEvent) => {
stopEvent(e); stopEvent(e);
toggleStoryPinned({ storyId: story.id, isPinned: true }); toggleStoryPinned({ peerId, storyId: story.id, isPinned: true });
showNotification({ showNotification({
message: lang('Story.ToastSavedToProfileText'), message: lang('Story.ToastSavedToProfileText'),
}); });
@ -100,7 +102,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const handleUnpinClick = useLastCallback((e: React.SyntheticEvent) => { const handleUnpinClick = useLastCallback((e: React.SyntheticEvent) => {
stopEvent(e); stopEvent(e);
toggleStoryPinned({ storyId: story.id, isPinned: false }); toggleStoryPinned({ peerId, storyId: story.id, isPinned: false });
showNotification({ showNotification({
message: lang('Story.ToastRemovedFromProfileText'), message: lang('Story.ToastRemovedFromProfileText'),
}); });
@ -125,6 +127,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
{thumbUrl && ( {thumbUrl && (
<img src={thumbUrl} alt="" className={styles.media} draggable={false} /> <img src={thumbUrl} alt="" className={styles.media} draggable={false} />
)} )}
{isFullyLoaded && <MediaAreaOverlay story={story} />}
{isProtected && <span className="protector" />} {isProtected && <span className="protector" />}
</div> </div>
{contextMenuPosition !== undefined && ( {contextMenuPosition !== undefined && (

View File

@ -2,20 +2,21 @@ import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useMemo, useRef, useState, memo, useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiStealthMode, ApiStory, ApiTypeStory, ApiUser, ApiPeer, ApiStealthMode, ApiStory, ApiTypeStory,
} from '../../api/types'; } from '../../api/types';
import type { IDimensions } from '../../global/types'; import type { IDimensions } from '../../global/types';
import type { Signal } from '../../util/signals'; import type { Signal } from '../../util/signals';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../../config'; import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../../config';
import { getUserFirstOrLastName } from '../../global/helpers'; import { getSenderTitle, isUserId } from '../../global/helpers';
import { import {
selectChat, selectIsCurrentUserPremium, selectChat, selectIsCurrentUserPremium,
selectTabState, selectUserStories, selectUserStory, selectPeerStories, selectPeerStory,
selectTabState, selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import captureKeyboardListeners from '../../util/captureKeyboardListeners';
@ -41,21 +42,21 @@ import useStoryPreloader from './hooks/useStoryPreloader';
import useStoryProps from './hooks/useStoryProps'; import useStoryProps from './hooks/useStoryProps';
import Avatar from '../common/Avatar'; import Avatar from '../common/Avatar';
import AvatarList from '../common/AvatarList';
import Composer from '../common/Composer'; import Composer from '../common/Composer';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import OptimizedVideo from '../ui/OptimizedVideo'; import OptimizedVideo from '../ui/OptimizedVideo';
import Skeleton from '../ui/placeholder/Skeleton'; import Skeleton from '../ui/placeholder/Skeleton';
import MediaAreaOverlay from './MediaAreaOverlay'; import MediaAreaOverlay from './mediaArea/MediaAreaOverlay';
import StoryCaption from './StoryCaption'; import StoryCaption from './StoryCaption';
import StoryFooter from './StoryFooter';
import StoryProgress from './StoryProgress'; import StoryProgress from './StoryProgress';
import styles from './StoryViewer.module.scss'; import styles from './StoryViewer.module.scss';
interface OwnProps { interface OwnProps {
userId: string; peerId: string;
storyId: number; storyId: number;
dimensions: IDimensions; dimensions: IDimensions;
// eslint-disable-next-line react/no-unused-prop-types // eslint-disable-next-line react/no-unused-prop-types
@ -66,16 +67,15 @@ interface OwnProps {
isArchivedStories?: boolean; isArchivedStories?: boolean;
isSingleStory?: boolean; isSingleStory?: boolean;
getIsAnimating: Signal<boolean>; getIsAnimating: Signal<boolean>;
onDelete: (storyId: number) => void; onDelete: (story: ApiTypeStory) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onReport: NoneToVoidFunction; onReport: NoneToVoidFunction;
} }
interface StateProps { interface StateProps {
user: ApiUser; peer: ApiPeer;
story?: ApiTypeStory; story?: ApiTypeStory;
isMuted: boolean; isMuted: boolean;
isSelf: boolean;
orderedIds?: number[]; orderedIds?: number[];
shouldForcePause?: boolean; shouldForcePause?: boolean;
storyChangelogUserId?: string; storyChangelogUserId?: string;
@ -95,10 +95,9 @@ const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E';
const STEALTH_MODE_NOTIFICATION_DURATION = 4000; const STEALTH_MODE_NOTIFICATION_DURATION = 4000;
function Story({ function Story({
isSelf, peerId,
userId,
storyId, storyId,
user, peer,
isMuted, isMuted,
isArchivedStories, isArchivedStories,
isPrivateStories, isPrivateStories,
@ -123,9 +122,8 @@ function Story({
setStoryViewerMuted, setStoryViewerMuted,
openPreviousStory, openPreviousStory,
openNextStory, openNextStory,
loadUserSkippedStories, loadPeerSkippedStories,
openForwardMenu, openForwardMenu,
openStoryViewModal,
copyStoryLink, copyStoryLink,
toggleStoryPinned, toggleStoryPinned,
openChat, openChat,
@ -163,31 +161,32 @@ function Story({
altMediaData, altMediaData,
hasFullData, hasFullData,
hasThumb, hasThumb,
mediaAreas,
canDownload, canDownload,
downloadMediaData, downloadMediaData,
} = useStoryProps(story, isCurrentUserPremium, isDropdownMenuOpen); } = useStoryProps(story, isCurrentUserPremium, isDropdownMenuOpen);
const isLoadedStory = story && 'content' in story; const isLoadedStory = story && 'content' in story;
const isChangelog = userId === storyChangelogUserId; const isChangelog = peerId === storyChangelogUserId;
const isChannel = !isUserId(peerId);
const isOut = isLoadedStory && story.isOut;
const canPinToProfile = useCurrentOrPrev( const canPinToProfile = useCurrentOrPrev(
isSelf && isLoadedStory ? !story.isPinned : undefined, isOut ? !story.isPinned : undefined,
true, true,
); );
const canUnpinFromProfile = useCurrentOrPrev( const canUnpinFromProfile = useCurrentOrPrev(
isSelf && isLoadedStory ? story.isPinned : undefined, isOut ? story.isPinned : undefined,
true, true,
); );
const areViewsExpired = Boolean( const areViewsExpired = Boolean(
isSelf && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(), isOut && (story!.date + viewersExpirePeriod) < getServerTime(),
); );
const canCopyLink = Boolean( const canCopyLink = Boolean(
isLoadedStory isLoadedStory
&& story.isPublic && story.isPublic
&& !isChangelog && !isChangelog
&& user?.usernames?.length, && peer?.usernames?.length,
); );
const canShare = Boolean( const canShare = Boolean(
@ -197,18 +196,14 @@ function Story({
&& !isChangelog && !isChangelog
&& !isCaptionExpanded, && !isCaptionExpanded,
); );
const canShareOwn = Boolean(
isSelf
&& isLoadedStory
&& story.isPublic
&& !story.noForwards,
);
const canPlayStory = Boolean( const canPlayStory = Boolean(
hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded
&& !isPausedBySpacebar && !isPausedByLongPress, && !isPausedBySpacebar && !isPausedByLongPress,
); );
const shouldShowFooter = isLoadedStory && (isOut || isChannel);
const { const {
shouldRender: shouldRenderSkeleton, transitionClassNames: skeletonTransitionClassNames, shouldRender: shouldRenderSkeleton, transitionClassNames: skeletonTransitionClassNames,
} = useShowTransition(!hasFullData); } = useShowTransition(!hasFullData);
@ -223,7 +218,7 @@ function Story({
const { const {
shouldRender: shouldRenderComposer, shouldRender: shouldRenderComposer,
transitionClassNames: composerAppearanceAnimationClassNames, transitionClassNames: composerAppearanceAnimationClassNames,
} = useShowTransition(!isSelf && !isChangelog); } = useShowTransition(!isOut && !isChangelog && !isChannel);
const { const {
shouldRender: shouldRenderCaptionBackdrop, shouldRender: shouldRenderCaptionBackdrop,
@ -232,29 +227,30 @@ function Story({
const { transitionClassNames: appearanceAnimationClassNames } = useShowTransition(true); const { transitionClassNames: appearanceAnimationClassNames } = useShowTransition(true);
useStoryPreloader(userId, storyId); useStoryPreloader(peerId, storyId);
useEffect(() => { useEffect(() => {
if (storyId) { if (storyId) {
viewStory({ userId, storyId }); viewStory({ peerId, storyId });
} }
}, [storyId, userId]); }, [storyId, peerId]);
useEffect(() => { useEffect(() => {
loadUserSkippedStories({ userId }); loadPeerSkippedStories({ peerId });
}, [userId]); }, [peerId]);
// Fetching user privacy settings for use in Composer // Fetching user privacy settings for use in Composer
useEffect(() => { useEffect(() => {
if (!isChatExist) { const canWrite = isUserId(peerId);
fetchChat({ chatId: userId }); if (!isChatExist && canWrite) {
fetchChat({ chatId: peerId });
} }
}, [isChatExist, userId]); }, [isChatExist, peerId]);
useEffect(() => { useEffect(() => {
if (isChatExist && !areChatSettingsLoaded) { if (isChatExist && !areChatSettingsLoaded) {
loadChatSettings({ chatId: userId }); loadChatSettings({ chatId: peerId });
} }
}, [areChatSettingsLoaded, isChatExist, userId]); }, [areChatSettingsLoaded, isChatExist, peerId]);
const handlePauseStory = useLastCallback(() => { const handlePauseStory = useLastCallback(() => {
if (isVideo) { if (isVideo) {
@ -310,11 +306,11 @@ function Story({
}, [hasAllData]); }, [hasAllData]);
useEffect(() => { useEffect(() => {
if (!isSelf || isDeletedStory || areViewsExpired) return; if (!isOut || isDeletedStory || areViewsExpired) return;
// Refresh recent viewers list each time // Refresh recent viewers list each time
loadStoryViews({ storyId, isPreload: true }); loadStoryViews({ peerId, storyId, isPreload: true });
}, [isDeletedStory, areViewsExpired, isSelf, storyId]); }, [isDeletedStory, areViewsExpired, isOut, peerId, storyId]);
useEffect(() => { useEffect(() => {
if ( if (
@ -382,7 +378,7 @@ function Story({
const handleOpenChat = useLastCallback(() => { const handleOpenChat = useLastCallback(() => {
onClose(); onClose();
openChat({ id: userId }); openChat({ id: peerId });
}); });
const handleOpenPrevStory = useLastCallback(() => { const handleOpenPrevStory = useLastCallback(() => {
@ -403,20 +399,20 @@ function Story({
}, [getIsAnimating, isComposerHasFocus]); }, [getIsAnimating, isComposerHasFocus]);
const handleCopyStoryLink = useLastCallback(() => { const handleCopyStoryLink = useLastCallback(() => {
copyStoryLink({ userId, storyId }); copyStoryLink({ peerId, storyId });
}); });
const handlePinClick = useLastCallback(() => { const handlePinClick = useLastCallback(() => {
toggleStoryPinned({ storyId, isPinned: true }); toggleStoryPinned({ peerId, storyId, isPinned: true });
}); });
const handleUnpinClick = useLastCallback(() => { const handleUnpinClick = useLastCallback(() => {
toggleStoryPinned({ storyId, isPinned: false }); toggleStoryPinned({ peerId, storyId, isPinned: false });
}); });
const handleDeleteStoryClick = useLastCallback(() => { const handleDeleteStoryClick = useLastCallback(() => {
setCurrentTime(0); setCurrentTime(0);
onDelete(story!.id); onDelete(story!);
}); });
const handleReportStoryClick = useLastCallback(() => { const handleReportStoryClick = useLastCallback(() => {
@ -424,12 +420,7 @@ function Story({
}); });
const handleForwardClick = useLastCallback(() => { const handleForwardClick = useLastCallback(() => {
openForwardMenu({ fromChatId: userId, storyId }); openForwardMenu({ fromChatId: peerId, storyId });
handlePauseStory();
});
const handleOpenStoryViewModal = useLastCallback(() => {
openStoryViewModal({ storyId });
}); });
const handleInfoPrivacyEdit = useLastCallback(() => { const handleInfoPrivacyEdit = useLastCallback(() => {
@ -442,7 +433,7 @@ function Story({
: story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody'); : story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody');
let message; let message;
const myName = getUserFirstOrLastName(user); const myName = getSenderTitle(lang, peer);
switch (visibility) { switch (visibility) {
case 'nobody': case 'nobody':
message = lang('StorySelectedContactsHint', myName); message = lang('StorySelectedContactsHint', myName);
@ -487,7 +478,7 @@ function Story({
const handleDownload = useLastCallback(() => { const handleDownload = useLastCallback(() => {
if (!downloadMediaData) return; if (!downloadMediaData) return;
download(downloadMediaData, `story-${userId}-${storyId}.${isVideo ? 'mp4' : 'jpg'}`); download(downloadMediaData, `story-${peerId}-${storyId}.${isVideo ? 'mp4' : 'jpg'}`);
}); });
useEffect(() => { useEffect(() => {
@ -539,6 +530,8 @@ function Story({
} }
function renderStoryPrivacyButton() { function renderStoryPrivacyButton() {
if (isChannel) return undefined;
let privacyIcon = 'channel-filled'; let privacyIcon = 'channel-filled';
const gradient: Record<string, [string, string]> = { const gradient: Record<string, [string, string]> = {
'channel-filled': ['#50ABFF', '#007AFF'], 'channel-filled': ['#50ABFF', '#007AFF'],
@ -547,7 +540,7 @@ function Story({
'group-filled': ['#FFB743', '#F69A36'], 'group-filled': ['#FFB743', '#F69A36'],
}; };
if (isSelf) { if (isOut) {
const { visibility } = (story && 'visibility' in story && story.visibility) || {}; const { visibility } = (story && 'visibility' in story && story.visibility) || {};
switch (visibility) { switch (visibility) {
@ -575,12 +568,12 @@ function Story({
return ( return (
<div <div
className={buildClassName(styles.visibilityButton, isSelf && styles.visibilityButtonSelf)} className={buildClassName(styles.visibilityButton, isOut && styles.visibilityButtonSelf)}
onClick={isSelf ? handleInfoPrivacyEdit : handleInfoPrivacyClick} onClick={isOut ? handleInfoPrivacyEdit : handleInfoPrivacyClick}
style={`--color-from: ${gradient[privacyIcon][0]}; --color-to: ${gradient[privacyIcon][1]}`} style={`--color-from: ${gradient[privacyIcon][0]}; --color-to: ${gradient[privacyIcon][1]}`}
> >
<i className={`icon icon-${privacyIcon}`} aria-hidden /> <i className={`icon icon-${privacyIcon}`} aria-hidden />
{isSelf && <i className="icon icon-next" aria-hidden />} {isOut && <i className="icon icon-next" aria-hidden />}
</div> </div>
); );
} }
@ -589,13 +582,13 @@ function Story({
return ( return (
<div className={styles.sender}> <div className={styles.sender}>
<Avatar <Avatar
peer={user} peer={peer}
size="tiny" size="tiny"
onClick={handleOpenChat} onClick={handleOpenChat}
/> />
<div className={styles.senderInfo}> <div className={styles.senderInfo}>
<span onClick={handleOpenChat} className={styles.senderName}> <span onClick={handleOpenChat} className={styles.senderName}>
{renderText(getUserFirstOrLastName(user) || '')} {renderText(getSenderTitle(lang, peer) || '')}
</span> </span>
<div className={styles.storyMetaRow}> <div className={styles.storyMetaRow}>
{story && 'date' in story && ( {story && 'date' in story && (
@ -650,62 +643,14 @@ function Story({
</MenuItem> </MenuItem>
)} )}
<MenuItem icon="eye-closed-outline" onClick={handleOpenStealthModal}>{lang('StealthMode')}</MenuItem> <MenuItem icon="eye-closed-outline" onClick={handleOpenStealthModal}>{lang('StealthMode')}</MenuItem>
{!isSelf && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>} {!isOut && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>}
{isSelf && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>} {isOut && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
); );
} }
const recentViewers = useMemo(() => {
const { users: { byId: usersById } } = getGlobal();
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, reactionsCount } = story as ApiStory;
if (!viewsCount) {
return (
<div className={buildClassName(styles.recentViewers, appearanceAnimationClassNames)}>
{lang('NobodyViewed')}
</div>
);
}
return (
<div
className={buildClassName(
styles.recentViewers,
styles.recentViewersInteractive,
appearanceAnimationClassNames,
)}
onClick={handleOpenStoryViewModal}
>
{!areViewsExpired && Boolean(recentViewers?.length) && (
<AvatarList
size="small"
peers={recentViewers}
className={styles.recentViewersAvatars}
/>
)}
<span>{lang('Views', viewsCount, 'i')}</span>
{Boolean(reactionsCount) && (
<span className={styles.reactionCount}>
<i className={buildClassName(styles.reactionCountHeart, 'icon icon-heart')} />
{reactionsCount}
</span>
)}
</div>
);
}
return ( return (
<div <div
className={buildClassName(styles.slideInner, 'component-theme-dark')} className={buildClassName(styles.slideInner, 'component-theme-dark')}
@ -781,21 +726,13 @@ function Story({
/> />
</> </>
)} )}
<MediaAreaOverlay mediaAreas={mediaAreas} mediaDimensions={dimensions} /> {isLoadedStory && fullMediaData && (
<MediaAreaOverlay story={story} className={styles.mediaAreaOverlay} isActive />
)}
</div> </div>
{isSelf && renderRecentViewers()} {shouldShowFooter && (
{canShareOwn && ( <StoryFooter story={story} className={appearanceAnimationClassNames} areViewsExpired={areViewsExpired} />
<Button
className={styles.ownForward}
color="translucent"
size="smaller"
round
onClick={handleForwardClick}
ariaLabel={lang('Forward')}
>
<i className="icon icon-forward" aria-hidden />
</Button>
)} )}
{shouldRenderCaptionBackdrop && ( {shouldRenderCaptionBackdrop && (
<div <div
@ -809,7 +746,7 @@ function Story({
{hasText && <div className={styles.captionGradient} />} {hasText && <div className={styles.captionGradient} />}
{hasText && ( {hasText && (
<StoryCaption <StoryCaption
key={`caption-${storyId}-${userId}`} key={`caption-${storyId}-${peerId}`}
story={story as ApiStory} story={story as ApiStory}
isExpanded={isCaptionExpanded} isExpanded={isCaptionExpanded}
onExpand={expandCaption} onExpand={expandCaption}
@ -820,10 +757,10 @@ function Story({
{shouldRenderComposer && ( {shouldRenderComposer && (
<Composer <Composer
type="story" type="story"
chatId={userId} chatId={peerId}
threadId={MAIN_THREAD_ID} threadId={MAIN_THREAD_ID}
storyId={storyId} storyId={storyId}
isReady={!isSelf} isReady={!isOut}
messageListType="thread" messageListType="thread"
isMobile={getIsMobile()} isMobile={getIsMobile()}
editableInputCssSelector={EDITABLE_STORY_INPUT_CSS_SELECTOR} editableInputCssSelector={EDITABLE_STORY_INPUT_CSS_SELECTOR}
@ -841,11 +778,11 @@ function Story({
} }
export default memo(withGlobal<OwnProps>((global, { export default memo(withGlobal<OwnProps>((global, {
userId, storyId, isPrivateStories, isArchivedStories, isReportModalOpen, isDeleteModalOpen, peerId, storyId, isPrivateStories, isArchivedStories, isReportModalOpen, isDeleteModalOpen,
}): StateProps => { }): StateProps => {
const { currentUserId, appConfig } = global; const { appConfig } = global;
const user = global.users.byId[userId]; const user = selectUser(global, peerId);
const chat = selectChat(global, userId); const chat = selectChat(global, peerId);
const tabState = selectTabState(global); const tabState = selectTabState(global);
const { const {
storyViewer: { storyViewer: {
@ -860,19 +797,18 @@ export default memo(withGlobal<OwnProps>((global, {
mapModal, mapModal,
} = tabState; } = tabState;
const { isOpen: isPremiumModalOpen } = premiumModal || {}; const { isOpen: isPremiumModalOpen } = premiumModal || {};
const { orderedIds, pinnedIds, archiveIds } = selectUserStories(global, userId) || {}; const { orderedIds, pinnedIds, archiveIds } = selectPeerStories(global, peerId) || {};
const story = selectUserStory(global, userId, storyId); const story = selectPeerStory(global, peerId, storyId);
const shouldForcePause = Boolean( const shouldForcePause = Boolean(
viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
|| isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal, || isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal,
); );
return { return {
user, peer: (user || chat)!,
story, story,
orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds), orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds),
isMuted, isMuted,
isSelf: currentUserId === userId,
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
shouldForcePause, shouldForcePause,
storyChangelogUserId: appConfig!.storyChangelogUserId, storyChangelogUserId: appConfig!.storyChangelogUserId,

View File

@ -1,30 +1,34 @@
import React, { memo, useCallback } from '../../lib/teact/teact'; import React, { memo, useCallback } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiTypeStory } from '../../api/types';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import ConfirmDialog from '../ui/ConfirmDialog'; import ConfirmDialog from '../ui/ConfirmDialog';
interface OwnProps { interface OwnProps {
isOpen: boolean; isOpen: boolean;
storyId?: number; story?: ApiTypeStory;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
} }
function StoryDeleteConfirmModal({ isOpen, storyId, onClose }: OwnProps) { function StoryDeleteConfirmModal({
isOpen, story, onClose,
}: OwnProps) {
const { deleteStory, openNextStory } = getActions(); const { deleteStory, openNextStory } = getActions();
const lang = useLang(); const lang = useLang();
const handleDeleteStoryClick = useCallback(() => { const handleDeleteStoryClick = useCallback(() => {
if (!storyId) { if (!story) {
return; return;
} }
openNextStory(); openNextStory();
deleteStory({ storyId }); deleteStory({ peerId: story.peerId, storyId: story.id });
onClose(); onClose();
}, [onClose, storyId]); }, [onClose, story]);
return ( return (
<ConfirmDialog <ConfirmDialog

View File

@ -0,0 +1,84 @@
.root {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
transition: opacity 350ms !important;
color: #fff;
@media (max-width: 600px) {
padding: 0 0.5rem 0.5rem;
}
@supports (margin-bottom: env(safe-area-inset-bottom)) {
margin-bottom: env(safe-area-inset-bottom);
}
}
.viewInfo {
display: flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius-default);
transition: background-color 200ms;
}
.interactive {
cursor: var(--custom-cursor, pointer);
&:hover {
background-color: rgba(var(--color-text-secondary-rgb), 0.2);
}
}
.avatars {
margin-inline-end: 0.5rem;
}
.spacer {
flex-grow: 1;
}
.reactionCount {
margin-inline-start: 0.5rem;
display: flex;
gap: 0.125rem;
align-items: center;
}
.reactionCountHeart {
color: var(--color-heart);
font-size: 1.25rem;
}
.channelReaction {
display: flex;
align-items: center;
padding-inline-end: 0.5rem;
}
.views {
display: flex;
align-items: center;
gap: 0.25rem;
}
.viewIcon {
font-size: 1.5rem;
}
.reactionButton {
--custom-emoji-size: 1.5rem;
overflow: visible !important;
:global(.icon-heart) {
transition: color 0.2s ease-out;
}
}
.reactionHeart {
color: var(--color-heart) !important;
}

View File

@ -0,0 +1,160 @@
import React, { memo, useMemo } from '../../lib/teact/teact';
import { getActions, getGlobal } from '../../global';
import type { ApiStory } from '../../api/types';
import { HEART_REACTION } from '../../config';
import { getStoryKey, isUserId } from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import AvatarList from '../common/AvatarList';
import Icon from '../common/Icon';
import ReactionAnimatedEmoji from '../common/reactions/ReactionAnimatedEmoji';
import Button from '../ui/Button';
import styles from './StoryFooter.module.scss';
type OwnProps = {
story: ApiStory;
areViewsExpired?: boolean;
className?: string;
};
const StoryFooter = ({
story,
areViewsExpired,
className,
}: OwnProps) => {
const { openStoryViewModal, openForwardMenu, sendStoryReaction } = getActions();
const lang = useLang();
const {
viewsCount, reactionsCount, isOut, peerId, id: storyId, sentReaction,
} = story;
const isChannel = !isUserId(peerId);
const isSentStoryReactionHeart = sentReaction && 'emoticon' in sentReaction
? sentReaction.emoticon === HEART_REACTION.emoticon : false;
const canForward = Boolean(
(isOut || isChannel)
&& story.isPublic
&& !story.noForwards,
);
const containerId = getStoryKey(peerId, storyId);
const recentViewers = useMemo(() => {
const { users: { byId: usersById } } = getGlobal();
const recentViewerIds = story && 'recentViewerIds' in story ? story.recentViewerIds : undefined;
if (!recentViewerIds) return undefined;
return recentViewerIds.map((id) => usersById[id]).filter(Boolean);
}, [story]);
const handleOpenStoryViewModal = useLastCallback(() => {
openStoryViewModal({ storyId });
});
const handleForwardClick = useLastCallback(() => {
openForwardMenu({ fromChatId: peerId, storyId });
});
const handleLikeStory = useLastCallback(() => {
const reaction = sentReaction ? undefined : HEART_REACTION;
sendStoryReaction({
peerId,
storyId,
containerId,
reaction,
});
});
if (!viewsCount) {
return (
<div className={buildClassName(styles.root, className)}>
{lang('NobodyViewed')}
</div>
);
}
return (
<div
className={buildClassName(
styles.root,
className,
)}
>
<div
className={buildClassName(styles.viewInfo, !isChannel && styles.interactive)}
onClick={!isChannel ? handleOpenStoryViewModal : undefined}
>
{!areViewsExpired && Boolean(recentViewers?.length) && (
<AvatarList
size="small"
peers={recentViewers}
className={styles.avatars}
/>
)}
{isChannel ? (
<span className={styles.views}><Icon name="channelviews" className={styles.viewIcon} />{viewsCount}</span>
) : (
<span className={styles.views}>{lang('Views', viewsCount, 'i')}</span>
)}
{Boolean(reactionsCount) && !isChannel && (
<span className={styles.reactionCount}>
<Icon name="heart" className={styles.reactionCountHeart} />
{reactionsCount}
</span>
)}
</div>
<div className={styles.spacer} />
{canForward && (
<Button
color="translucent"
size="smaller"
round
onClick={handleForwardClick}
ariaLabel={lang('Forward')}
>
<Icon name="forward" />
</Button>
)}
{isChannel && (
<div className={styles.channelReaction}>
<Button
round
className={styles.reactionButton}
color="translucent"
size="smaller"
onClick={handleLikeStory}
ariaLabel={lang('AccDescrLike')}
>
{sentReaction && (
<ReactionAnimatedEmoji
key={'documentId' in sentReaction ? sentReaction.documentId : sentReaction.emoticon}
containerId={containerId}
reaction={sentReaction}
withEffectOnly={isSentStoryReactionHeart}
/>
)}
{(!sentReaction || isSentStoryReactionHeart) && (
<Icon
name={isSentStoryReactionHeart ? 'heart' : 'heart-outline'}
className={buildClassName(isSentStoryReactionHeart && styles.reactionHeart)}
/>
)}
</Button>
{Boolean(reactionsCount) && (<span>{reactionsCount}</span>)}
</div>
)}
</div>
);
};
export default memo(StoryFooter);

View File

@ -1,22 +1,26 @@
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 type { ApiTypeStory, ApiUser, ApiUserStories } from '../../api/types'; import type {
ApiPeer, ApiPeerStories, ApiTypeStory,
} from '../../api/types';
import type { StoryViewerOrigin } from '../../types'; import type { StoryViewerOrigin } from '../../types';
import { getStoryMediaHash, getUserFirstOrLastName } from '../../global/helpers'; import { getSenderTitle, getStoryMediaHash } from '../../global/helpers';
import { selectTabState } from '../../global/selectors'; import { selectTabState } from '../../global/selectors';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import Avatar from '../common/Avatar'; import Avatar from '../common/Avatar';
import MediaAreaOverlay from './mediaArea/MediaAreaOverlay';
import styles from './StoryViewer.module.scss'; import styles from './StoryViewer.module.scss';
interface OwnProps { interface OwnProps {
user?: ApiUser; peer?: ApiPeer;
userStories?: ApiUserStories; peerStories?: ApiPeerStories;
} }
interface StateProps { interface StateProps {
@ -25,73 +29,75 @@ interface StateProps {
} }
function StoryPreview({ function StoryPreview({
user, userStories, lastViewedId, origin, peer, peerStories, lastViewedId, origin,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { openStoryViewer, loadUserSkippedStories } = getActions(); const { openStoryViewer, loadPeerSkippedStories } = getActions();
const lang = useLang();
const story = useMemo<ApiTypeStory | undefined>(() => { const story = useMemo<ApiTypeStory | undefined>(() => {
if (!userStories) { if (!peerStories) {
return undefined; return undefined;
} }
const { const {
orderedIds, lastReadId, byId, orderedIds, lastReadId, byId,
} = userStories; } = peerStories;
const hasUnreadStories = orderedIds[orderedIds.length - 1] !== lastReadId; const hasUnreadStories = orderedIds[orderedIds.length - 1] !== lastReadId;
const previewIndexId = lastViewedId ?? (hasUnreadStories ? (lastReadId ?? -1) : -1); const previewIndexId = lastViewedId ?? (hasUnreadStories ? (lastReadId ?? -1) : -1);
const resultId = byId[previewIndexId]?.id || orderedIds[0]; const resultId = byId[previewIndexId]?.id || orderedIds[0];
return byId[resultId]; return byId[resultId];
}, [lastViewedId, userStories]); }, [lastViewedId, peerStories]);
const isLoaded = story && 'content' in story;
useEffect(() => { useEffect(() => {
if (story && !('content' in story)) { if (story && !isLoaded) {
loadUserSkippedStories({ userId: story.userId }); loadPeerSkippedStories({ peerId: story.peerId });
} }
}, [story]); }, [story, isLoaded]);
const video = story && 'content' in story ? story.content.video : undefined; const video = isLoaded ? story.content.video : undefined;
const imageHash = story && 'content' in story const imageHash = isLoaded ? getStoryMediaHash(story) : undefined;
? getStoryMediaHash(story)
: undefined;
const imgBlobUrl = useMedia(imageHash); const imgBlobUrl = useMedia(imageHash);
const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri; const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri;
if (!user || !story || 'isDeleted' in story) { if (!peer || !story || 'isDeleted' in story) {
return undefined; return undefined;
} }
return ( return (
<div <div
className={styles.slideInner} className={styles.slideInner}
onClick={() => { openStoryViewer({ userId: story.userId, storyId: story.id, origin }); }} onClick={() => { openStoryViewer({ peerId: story.peerId, storyId: story.id, origin }); }}
> >
{thumbUrl && ( {thumbUrl && (
<img src={thumbUrl} alt="" className={styles.media} draggable={false} /> <img src={thumbUrl} alt="" className={styles.media} draggable={false} />
)} )}
{isLoaded && <MediaAreaOverlay story={story} />}
<div className={styles.content}> <div className={styles.content}>
<Avatar <Avatar
peer={user} peer={peer}
withStory withStory
storyViewerMode="disabled" storyViewerMode="disabled"
/> />
<div className={styles.name}>{renderText(getUserFirstOrLastName(user) || '')}</div> <div className={styles.name}>{renderText(getSenderTitle(lang, peer) || '')}</div>
</div> </div>
</div> </div>
); );
} }
export default memo(withGlobal<OwnProps>((global, { user }): StateProps => { export default memo(withGlobal<OwnProps>((global, { peer }): StateProps => {
const { const {
storyViewer: { storyViewer: {
lastViewedByUserIds, lastViewedByPeerIds,
origin, origin,
}, },
} = selectTabState(global); } = selectTabState(global);
return { return {
lastViewedId: user?.id ? lastViewedByUserIds?.[user.id] : undefined, lastViewedId: peer?.id ? lastViewedByPeerIds?.[peer.id] : undefined,
origin, origin,
}; };
})(StoryPreview)); })(StoryPreview));

View File

@ -18,7 +18,7 @@
opacity: 0; opacity: 0;
} }
.user { .peer {
flex: 0 0 3.75rem; flex: 0 0 3.75rem;
width: 3.75rem; width: 3.75rem;
display: flex; display: flex;

View File

@ -1,7 +1,7 @@
import React, { memo, useRef } from '../../lib/teact/teact'; import React, { memo, useRef } from '../../lib/teact/teact';
import { withGlobal } from '../../global'; import { withGlobal } from '../../global';
import type { ApiUser } from '../../api/types'; import type { ApiChat, ApiUser } from '../../api/types';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -20,17 +20,23 @@ interface OwnProps {
} }
interface StateProps { interface StateProps {
orderedUserIds: string[]; orderedPeerIds: string[];
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
chatsById: Record<string, ApiChat>;
} }
function StoryRibbon({ function StoryRibbon({
isArchived, className, orderedUserIds, usersById, isClosing, isArchived,
className,
orderedPeerIds,
usersById,
chatsById,
isClosing,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const lang = useLang(); const lang = useLang();
const fullClassName = buildClassName( const fullClassName = buildClassName(
styles.root, styles.root,
!orderedUserIds.length && styles.hidden, !orderedPeerIds.length && styles.hidden,
isClosing && styles.closing, isClosing && styles.closing,
className, className,
'no-scrollbar', 'no-scrollbar',
@ -48,17 +54,17 @@ function StoryRibbon({
className={fullClassName} className={fullClassName}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >
{orderedUserIds.map((userId) => { {orderedPeerIds.map((peerId) => {
const user = usersById[userId]; const peer = usersById[peerId] || chatsById[peerId];
if (!user) { if (!peer) {
return undefined; return undefined;
} }
return ( return (
<StoryRibbonButton <StoryRibbonButton
key={userId} key={peerId}
user={user} peer={peer}
isArchived={isArchived} isArchived={isArchived}
/> />
); );
@ -69,12 +75,14 @@ function StoryRibbon({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { isArchived }): StateProps => { (global, { isArchived }): StateProps => {
const { orderedUserIds: { active, archived } } = global.stories; const { orderedPeerIds: { active, archived } } = global.stories;
const usersById = global.users.byId; const usersById = global.users.byId;
const chatsById = global.chats.byId;
return { return {
orderedUserIds: isArchived ? archived : active, orderedPeerIds: isArchived ? archived : active,
usersById, usersById,
chatsById,
}; };
}, },
)(StoryRibbon)); )(StoryRibbon));

View File

@ -1,10 +1,10 @@
import React, { memo, useRef } from '../../lib/teact/teact'; import React, { memo, useRef } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiUser } from '../../api/types'; import type { ApiPeer } from '../../api/types';
import { StoryViewerOrigin } from '../../types'; import { StoryViewerOrigin } from '../../types';
import { getUserFirstOrLastName } from '../../global/helpers'; import { getSenderTitle, isUserId } from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
@ -21,11 +21,11 @@ import MenuItem from '../ui/MenuItem';
import styles from './StoryRibbon.module.scss'; import styles from './StoryRibbon.module.scss';
interface OwnProps { interface OwnProps {
user: ApiUser; peer: ApiPeer;
isArchived?: boolean; isArchived?: boolean;
} }
function StoryRibbonButton({ user, isArchived }: OwnProps) { function StoryRibbonButton({ peer, isArchived }: OwnProps) {
const { const {
openChat, openChat,
openChatWithInfo, openChatWithInfo,
@ -37,7 +37,10 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useStoryPreloader(user.id); const isSelf = 'isSelf' in peer && peer.isSelf;
const isChannel = !isUserId(peer.id);
useStoryPreloader(peer.id);
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuPosition,
@ -47,7 +50,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
const getTriggerElement = useLastCallback(() => ref.current); const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => document.body); const getRootElement = useLastCallback(() => document.body);
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-user-context-menu .bubble')); const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-peer-context-menu .bubble'));
const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true })); const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true }));
const { const {
@ -63,7 +66,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (isContextMenuOpen) return; if (isContextMenuOpen) return;
openStoryViewer({ userId: user.id, origin: StoryViewerOrigin.StoryRibbon }); openStoryViewer({ peerId: peer.id, origin: StoryViewerOrigin.StoryRibbon });
}); });
const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => { const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
@ -72,44 +75,44 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
}); });
const handleSavedStories = useLastCallback(() => { const handleSavedStories = useLastCallback(() => {
openChatWithInfo({ id: user.id, shouldReplaceHistory: true, profileTab: 'stories' }); openChatWithInfo({ id: peer.id, shouldReplaceHistory: true, profileTab: 'stories' });
}); });
const handleArchivedStories = useLastCallback(() => { const handleArchivedStories = useLastCallback(() => {
openChatWithInfo({ id: user.id, shouldReplaceHistory: true, profileTab: 'storiesArchive' }); openChatWithInfo({ id: peer.id, shouldReplaceHistory: true, profileTab: 'storiesArchive' });
}); });
const handleOpenChat = useLastCallback(() => { const handleOpenChat = useLastCallback(() => {
openChat({ id: user.id, shouldReplaceHistory: true }); openChat({ id: peer.id, shouldReplaceHistory: true });
}); });
const handleOpenProfile = useLastCallback(() => { const handleOpenProfile = useLastCallback(() => {
openChatWithInfo({ id: user.id, shouldReplaceHistory: true }); openChatWithInfo({ id: peer.id, shouldReplaceHistory: true });
}); });
const handleArchiveUser = useLastCallback(() => { const handleArchivePeer = useLastCallback(() => {
toggleStoriesHidden({ userId: user.id, isHidden: !isArchived }); toggleStoriesHidden({ peerId: peer.id, isHidden: !isArchived });
}); });
return ( return (
<div <div
ref={ref} ref={ref}
role="button" role="button"
data-peer-id={user.id} data-peer-id={peer.id}
tabIndex={0} tabIndex={0}
className={styles.user} className={styles.peer}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onClick={handleClick} onClick={handleClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<Avatar <Avatar
peer={user} peer={peer}
withStory withStory
storyViewerOrigin={StoryViewerOrigin.StoryRibbon} storyViewerOrigin={StoryViewerOrigin.StoryRibbon}
storyViewerMode="full" storyViewerMode="full"
/> />
<div className={buildClassName(styles.name, user.hasUnreadStories && styles.name_hasUnreadStory)}> <div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}>
{user.isSelf ? lang('MyStory') : getUserFirstOrLastName(user)} {isSelf ? lang('MyStory') : getSenderTitle(lang, peer)}
</div> </div>
{contextMenuPosition !== undefined && ( {contextMenuPosition !== undefined && (
<Menu <Menu
@ -119,13 +122,13 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
positionX={positionX} positionX={positionX}
positionY={positionY} positionY={positionY}
style={menuStyle} style={menuStyle}
className={buildClassName('story-user-context-menu', styles.contextMenu)} className={buildClassName('story-peer-context-menu', styles.contextMenu)}
autoClose autoClose
withPortal withPortal
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
> >
{user.isSelf ? ( {isSelf ? (
<> <>
<MenuItem onClick={handleSavedStories} icon="play-story"> <MenuItem onClick={handleSavedStories} icon="play-story">
{lang('StoryList.Context.SavedStories')} {lang('StoryList.Context.SavedStories')}
@ -136,14 +139,22 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
</> </>
) : ( ) : (
<> <>
<MenuItem onClick={handleOpenChat} icon="message"> {!isChannel && (
{lang('SendMessageTitle')} <MenuItem onClick={handleOpenChat} icon="message">
</MenuItem> {lang('SendMessageTitle')}
<MenuItem onClick={handleOpenProfile} icon="user"> </MenuItem>
{lang('StoryList.Context.ViewProfile')} )}
</MenuItem> {isChannel ? (
<MenuItem onClick={handleOpenProfile} icon="channel">
{lang('ChatList.ContextOpenChannel')}
</MenuItem>
) : (
<MenuItem onClick={handleOpenProfile} icon="user">
{lang('StoryList.Context.ViewProfile')}
</MenuItem>
)}
<MenuItem <MenuItem
onClick={handleArchiveUser} onClick={handleArchivePeer}
icon={isArchived ? 'unarchive' : 'archive'} icon={isArchived ? 'unarchive' : 'archive'}
> >
{lang(isArchived ? 'StoryList.Context.Unarchive' : 'StoryList.Context.Archive')} {lang(isArchived ? 'StoryList.Context.Unarchive' : 'StoryList.Context.Archive')}

View File

@ -7,8 +7,8 @@ import type { ApiStory, ApiUser } from '../../api/types';
import type { ApiPrivacySettings, PrivacyVisibility } from '../../types'; import type { ApiPrivacySettings, PrivacyVisibility } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { getUserFullName } from '../../global/helpers'; import { getSenderTitle, getUserFullName } from '../../global/helpers';
import { selectTabState, selectUserStory } from '../../global/selectors'; import { selectPeerStory, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
@ -191,11 +191,12 @@ function StorySettings({
const handleSubmit = useLastCallback(() => { const handleSubmit = useLastCallback(() => {
editStoryPrivacy({ editStoryPrivacy({
peerId: story!.peerId,
storyId: story!.id, storyId: story!.id,
privacy: privacy!, privacy: privacy!,
}); });
if (story!.isPinned !== isPinned) { if (story!.isPinned !== isPinned) {
toggleStoryPinned({ storyId: story!.id, isPinned }); toggleStoryPinned({ peerId: story!.peerId, storyId: story!.id, isPinned });
} }
closeModal(); closeModal();
}); });
@ -207,7 +208,7 @@ function StorySettings({
} }
if (closeFriendIds.length === 1) { if (closeFriendIds.length === 1) {
return getUserFullName(usersById[closeFriendIds[0]]); return getSenderTitle(lang, usersById[closeFriendIds[0]]);
} }
return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i'); return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i');
@ -401,11 +402,11 @@ function StorySettings({
export default memo(withGlobal<OwnProps>((global): StateProps => { export default memo(withGlobal<OwnProps>((global): StateProps => {
const { const {
storyViewer: { storyViewer: {
storyId, userId, storyId, peerId,
}, },
} = selectTabState(global); } = selectTabState(global);
const story = (userId && storyId) const story = (peerId && storyId)
? selectUserStory(global, userId, storyId) ? selectPeerStory(global, peerId, storyId)
: undefined; : undefined;
return { return {

View File

@ -3,11 +3,11 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiUserStories } from '../../api/types'; import type { ApiPeerStories, ApiTypeStory } from '../../api/types';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY } from '../../config';
import { getStoryKey } from '../../global/helpers'; import { getStoryKey } from '../../global/helpers';
import { selectIsStoryViewerOpen, selectTabState, selectUser } from '../../global/selectors'; import { selectIsStoryViewerOpen, selectPeer, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle'; import buildStyle from '../../util/buildStyle';
import { IS_FIREFOX, IS_SAFARI } from '../../util/windowEnvironment'; import { IS_FIREFOX, IS_SAFARI } from '../../util/windowEnvironment';
@ -29,17 +29,17 @@ interface OwnProps {
isOpen?: boolean; isOpen?: boolean;
isReportModalOpen?: boolean; isReportModalOpen?: boolean;
isDeleteModalOpen?: boolean; isDeleteModalOpen?: boolean;
onDelete: (storyId: number) => void; onDelete: (story: ApiTypeStory) => void;
onReport: NoneToVoidFunction; onReport: NoneToVoidFunction;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
} }
interface StateProps { interface StateProps {
userIds: string[]; peerIds: string[];
currentUserId?: string; currentPeerId?: string;
currentStoryId?: number; currentStoryId?: number;
byUserId?: Record<string, ApiUserStories>; byPeerId?: Record<string, ApiPeerStories>;
isSingleUser?: boolean; isSinglePeer?: boolean;
isSingleStory?: boolean; isSingleStory?: boolean;
isPrivate?: boolean; isPrivate?: boolean;
isArchive?: boolean; isArchive?: boolean;
@ -52,15 +52,15 @@ const ANIMATION_TO_ACTIVE_SCALE = '3';
const ANIMATION_FROM_ACTIVE_SCALE = `${FROM_ACTIVE_SCALE_VALUE}`; const ANIMATION_FROM_ACTIVE_SCALE = `${FROM_ACTIVE_SCALE_VALUE}`;
function StorySlides({ function StorySlides({
userIds, peerIds,
currentUserId, currentPeerId,
currentStoryId, currentStoryId,
isOpen, isOpen,
isSingleUser, isSinglePeer,
isSingleStory, isSingleStory,
isPrivate, isPrivate,
isArchive, isArchive,
byUserId, byPeerId,
isReportModalOpen, isReportModalOpen,
isDeleteModalOpen, isDeleteModalOpen,
onDelete, onDelete,
@ -68,12 +68,12 @@ function StorySlides({
onReport, onReport,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { stopActiveReaction } = getActions(); const { stopActiveReaction } = getActions();
const [renderingUserId, setRenderingUserId] = useState(currentUserId); const [renderingPeerId, setRenderingPeerId] = useState(currentPeerId);
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId); const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
const prevUserId = usePrevious(currentUserId); const prevPeerId = usePrevious(currentPeerId);
const renderingIsArchive = useCurrentOrPrev(isArchive, true); const renderingIsArchive = useCurrentOrPrev(isArchive, true);
const renderingIsPrivate = useCurrentOrPrev(isPrivate, true); const renderingIsPrivate = useCurrentOrPrev(isPrivate, true);
const renderingIsSingleUser = useCurrentOrPrev(isSingleUser, true); const renderingIsSinglePeer = useCurrentOrPrev(isSinglePeer, true);
const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true); const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true);
const slideSizes = useSlideSizes(); const slideSizes = useSlideSizes();
@ -86,62 +86,62 @@ function StorySlides({
shouldBeReplaced: true, shouldBeReplaced: true,
}); });
function setRef(ref: HTMLDivElement | null, userId: string) { function setRef(ref: HTMLDivElement | null, peerId: string) {
if (!ref) { if (!ref) {
return; return;
} }
if (!rendersRef.current[userId]) { if (!rendersRef.current[peerId]) {
rendersRef.current[userId] = { current: ref }; rendersRef.current[peerId] = { current: ref };
} else { } else {
rendersRef.current[userId].current = ref; rendersRef.current[peerId].current = ref;
} }
} }
const renderingUserIds = useMemo(() => { const renderingPeerIds = useMemo(() => {
if (renderingUserId && (renderingIsSingleUser || renderingIsSingleStory)) { if (renderingPeerId && (renderingIsSinglePeer || renderingIsSingleStory)) {
return [renderingUserId]; return [renderingPeerId];
} }
const index = renderingUserId ? userIds.indexOf(renderingUserId) : -1; const index = renderingPeerId ? peerIds.indexOf(renderingPeerId) : -1;
if (!renderingUserId || index === -1) { if (!renderingPeerId || index === -1) {
return []; return [];
} }
const start = Math.max(index - 4, 0); const start = Math.max(index - 4, 0);
const end = Math.min(index + 5, userIds.length); const end = Math.min(index + 5, peerIds.length);
return userIds.slice(start, end); return peerIds.slice(start, end);
}, [renderingIsSingleStory, renderingIsSingleUser, renderingUserId, userIds]); }, [renderingIsSingleStory, renderingIsSinglePeer, renderingPeerId, peerIds]);
const renderingUserPosition = useMemo(() => { const renderingPeerPosition = useMemo(() => {
if (!renderingUserIds.length || !renderingUserId) { if (!renderingPeerIds.length || !renderingPeerId) {
return -1; return -1;
} }
return renderingUserIds.indexOf(renderingUserId); return renderingPeerIds.indexOf(renderingPeerId);
}, [renderingUserId, renderingUserIds]); }, [renderingPeerId, renderingPeerIds]);
const currentUserPosition = useMemo(() => { const currentPeerPosition = useMemo(() => {
if (!renderingUserIds.length || !currentUserId) { if (!renderingPeerIds.length || !currentPeerId) {
return -1; return -1;
} }
return renderingUserIds.indexOf(currentUserId); return renderingPeerIds.indexOf(currentPeerId);
}, [currentUserId, renderingUserIds]); }, [currentPeerId, renderingPeerIds]);
useEffect(() => { useEffect(() => {
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
setRenderingUserId(currentUserId); setRenderingPeerId(currentPeerId);
}, ANIMATION_DURATION_MS); }, ANIMATION_DURATION_MS);
return () => { return () => {
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
}; };
}, [currentUserId]); }, [currentPeerId]);
useEffect(() => { useEffect(() => {
let timeOutId: number | undefined; let timeOutId: number | undefined;
if (renderingUserId !== currentUserId) { if (renderingPeerId !== currentPeerId) {
timeOutId = window.setTimeout(() => { timeOutId = window.setTimeout(() => {
setRenderingStoryId(currentStoryId); setRenderingStoryId(currentStoryId);
}, ANIMATION_DURATION_MS); }, ANIMATION_DURATION_MS);
@ -152,12 +152,12 @@ function StorySlides({
return () => { return () => {
window.clearTimeout(timeOutId); window.clearTimeout(timeOutId);
}; };
}, [renderingUserId, currentStoryId, currentUserId, renderingStoryId]); }, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId]);
useEffect(() => { useEffect(() => {
let timeOutId: number | undefined; let timeOutId: number | undefined;
if (prevUserId && prevUserId !== currentUserId) { if (prevPeerId && prevPeerId !== currentPeerId) {
setIsAnimating(true); setIsAnimating(true);
timeOutId = window.setTimeout(() => { timeOutId = window.setTimeout(() => {
setIsAnimating(false); setIsAnimating(false);
@ -168,24 +168,24 @@ function StorySlides({
setIsAnimating(false); setIsAnimating(false);
window.clearTimeout(timeOutId); window.clearTimeout(timeOutId);
}; };
}, [prevUserId, currentUserId, setIsAnimating]); }, [prevPeerId, currentPeerId, setIsAnimating]);
useEffect(() => { useEffect(() => {
return () => { return () => {
if (!currentStoryId || !currentUserId) return; if (!currentStoryId || !currentPeerId) return;
stopActiveReaction({ stopActiveReaction({
containerId: getStoryKey(currentUserId, currentStoryId), containerId: getStoryKey(currentPeerId, currentStoryId),
}); });
}; };
}, [currentStoryId, currentUserId]); }, [currentStoryId, currentPeerId]);
const slideAmount = currentUserPosition - renderingUserPosition; const slideAmount = currentPeerPosition - renderingPeerPosition;
const isBackward = renderingUserPosition > currentUserPosition; const isBackward = renderingPeerPosition > currentPeerPosition;
const calculateTransformX = useLastCallback(() => { const calculateTransformX = useLastCallback(() => {
return userIds.reduce<Record<string, number>>((transformX, userId, index) => { return peerIds.reduce<Record<string, number>>((transformX, peerId, index) => {
if (userId === renderingUserId) { if (peerId === renderingPeerId) {
transformX[userId] = calculateOffsetX({ transformX[peerId] = calculateOffsetX({
scale: slideSizes.scale, scale: slideSizes.scale,
slideAmount, slideAmount,
isBackward, isBackward,
@ -193,18 +193,18 @@ function StorySlides({
}); });
} else { } else {
let isMoveThroughActiveSlide = false; let isMoveThroughActiveSlide = false;
if (!isBackward && index > 0 && userIds[index - 1] === renderingUserId) { if (!isBackward && index > 0 && peerIds[index - 1] === renderingPeerId) {
isMoveThroughActiveSlide = true; isMoveThroughActiveSlide = true;
} }
if (isBackward && index < userIds.length - 1 && userIds[index + 1] === renderingUserId) { if (isBackward && index < peerIds.length - 1 && peerIds[index + 1] === renderingPeerId) {
isMoveThroughActiveSlide = true; isMoveThroughActiveSlide = true;
} }
transformX[userId] = calculateOffsetX({ transformX[peerId] = calculateOffsetX({
scale: slideSizes.scale, scale: slideSizes.scale,
slideAmount, slideAmount,
isBackward, isBackward,
isActiveSlideSize: currentUserId === userId && !isBackward, isActiveSlideSize: currentPeerId === peerId && !isBackward,
isMoveThroughActiveSlide, isMoveThroughActiveSlide,
}); });
} }
@ -216,7 +216,7 @@ function StorySlides({
useLayoutEffect(() => { useLayoutEffect(() => {
const transformX = calculateTransformX(); const transformX = calculateTransformX();
Object.entries(rendersRef.current).forEach(([userId, { current }]) => { Object.entries(rendersRef.current).forEach(([peerId, { current }]) => {
if (!current) return; if (!current) return;
if (!getIsAnimating()) { if (!getIsAnimating()) {
@ -228,28 +228,28 @@ function StorySlides({
return; return;
} }
const scale = currentUserId === userId const scale = currentPeerId === peerId
? ANIMATION_TO_ACTIVE_SCALE ? ANIMATION_TO_ACTIVE_SCALE
: userId === renderingUserId ? ANIMATION_FROM_ACTIVE_SCALE : '1'; : peerId === renderingPeerId ? ANIMATION_FROM_ACTIVE_SCALE : '1';
let offsetY = 0; let offsetY = 0;
if (userId === renderingUserId) { if (peerId === renderingPeerId) {
offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * FROM_ACTIVE_SCALE_VALUE; offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * FROM_ACTIVE_SCALE_VALUE;
current.classList.add(styles.slideAnimationFromActive); current.classList.add(styles.slideAnimationFromActive);
} }
if (userId === currentUserId) { if (peerId === currentPeerId) {
offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM; offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM;
current.classList.add(styles.slideAnimationToActive); current.classList.add(styles.slideAnimationToActive);
} }
current.classList.add(styles.slideAnimation); current.classList.add(styles.slideAnimation);
current.style.setProperty('--slide-translate-x', `${transformX[userId] || 0}px`); current.style.setProperty('--slide-translate-x', `${transformX[peerId] || 0}px`);
current.style.setProperty('--slide-translate-y', `${offsetY}rem`); current.style.setProperty('--slide-translate-y', `${offsetY}rem`);
current.style.setProperty('--slide-translate-scale', scale); current.style.setProperty('--slide-translate-scale', scale);
}); });
}, [currentUserId, getIsAnimating, renderingUserId]); }, [currentPeerId, getIsAnimating, renderingPeerId]);
function renderStoryPreview(userId: string, index: number, position: number) { function renderStoryPreview(peerId: string, index: number, position: number) {
const style = buildStyle( const style = buildStyle(
`width: ${slideSizes.slide.width}px`, `width: ${slideSizes.slide.width}px`,
`height: ${slideSizes.slide.height}px`, `height: ${slideSizes.slide.height}px`,
@ -262,20 +262,20 @@ function StorySlides({
return ( return (
<div <div
key={userId} key={peerId}
ref={(ref) => setRef(ref, userId)} ref={(ref) => setRef(ref, peerId)}
className={className} className={className}
style={style} style={style}
> >
<StoryPreview <StoryPreview
user={selectUser(getGlobal(), userId)} peer={selectPeer(getGlobal(), peerId)}
userStories={byUserId?.[userId]} peerStories={byPeerId?.[peerId]}
/> />
</div> </div>
); );
} }
function renderStory(userId: string) { function renderStory(peerId: string) {
const style = buildStyle( const style = buildStyle(
`width: ${slideSizes.activeSlide.width}px`, `width: ${slideSizes.activeSlide.width}px`,
`--slide-media-height: ${slideSizes.activeSlide.height}px`, `--slide-media-height: ${slideSizes.activeSlide.height}px`,
@ -283,13 +283,13 @@ function StorySlides({
return ( return (
<div <div
key={userId} key={peerId}
ref={(ref) => setRef(ref, userId)} ref={(ref) => setRef(ref, peerId)}
className={buildClassName(styles.slide, styles.activeSlide)} className={buildClassName(styles.slide, styles.activeSlide)}
style={style} style={style}
> >
<Story <Story
userId={userId} peerId={peerId}
storyId={renderingStoryId!} storyId={renderingStoryId!}
onDelete={onDelete} onDelete={onDelete}
dimensions={slideSizes.activeSlide} dimensions={slideSizes.activeSlide}
@ -309,15 +309,15 @@ function StorySlides({
return ( return (
<div className={styles.wrapper} style={`--story-viewer-scale: ${slideSizes.scale}`}> <div className={styles.wrapper} style={`--story-viewer-scale: ${slideSizes.scale}`}>
<div className={styles.fullSize} onClick={onClose} /> <div className={styles.fullSize} onClick={onClose} />
{renderingUserIds.length > 1 && ( {renderingPeerIds.length > 1 && (
<div className={styles.backdropNonInteractive} style={`height: ${slideSizes.slide.height}px`} /> <div className={styles.backdropNonInteractive} style={`height: ${slideSizes.slide.height}px`} />
)} )}
{renderingUserIds.map((userId, index) => { {renderingPeerIds.map((peerId, index) => {
if (userId === renderingUserId) { if (peerId === renderingPeerId) {
return renderStory(renderingUserId); return renderStory(renderingPeerId);
} }
return renderStoryPreview(userId, index, index - renderingUserPosition); return renderStoryPreview(peerId, index, index - renderingPeerPosition);
})} })}
</div> </div>
); );
@ -326,18 +326,18 @@ function StorySlides({
export default memo(withGlobal<OwnProps>((global): StateProps => { export default memo(withGlobal<OwnProps>((global): StateProps => {
const { const {
storyViewer: { storyViewer: {
userId: currentUserId, storyId: currentStoryId, isSingleUser, isSingleStory, isPrivate, isArchive, peerId: currentPeerId, storyId: currentStoryId, isSinglePeer, isSingleStory, isPrivate, isArchive,
}, },
} = selectTabState(global); } = selectTabState(global);
const { byUserId, orderedUserIds: { archived, active } } = global.stories; const { byPeerId, orderedPeerIds: { archived, active } } = global.stories;
const user = currentUserId ? selectUser(global, currentUserId) : undefined; const peer = currentPeerId ? selectPeer(global, currentPeerId) : undefined;
return { return {
byUserId, byPeerId,
userIds: user?.areStoriesHidden ? archived : active, peerIds: peer?.areStoriesHidden ? archived : active,
currentUserId, currentPeerId,
currentStoryId, currentStoryId,
isSingleUser, isSinglePeer,
isSingleStory, isSingleStory,
isPrivate, isPrivate,
isArchive, isArchive,

View File

@ -1,7 +1,7 @@
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 type { ApiUser } from '../../api/types'; import type { ApiChat, ApiUser } from '../../api/types';
import { ANIMATION_END_DELAY, PREVIEW_AVATAR_COUNT } from '../../config'; import { ANIMATION_END_DELAY, PREVIEW_AVATAR_COUNT } from '../../config';
import { selectPerformanceSettingsValue, selectTabState } from '../../global/selectors'; import { selectPerformanceSettingsValue, selectTabState } from '../../global/selectors';
@ -24,18 +24,20 @@ interface OwnProps {
interface StateProps { interface StateProps {
currentUserId: string; currentUserId: string;
orderedUserIds: string[]; orderedPeerIds: string[];
isShown: boolean; isShown: boolean;
withAnimation?: boolean; withAnimation?: boolean;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
chatsById: Record<string, ApiChat>;
} }
const PRELOAD_USERS = 5; const PRELOAD_PEERS = 5;
function StoryToggler({ function StoryToggler({
currentUserId, currentUserId,
orderedUserIds, orderedPeerIds,
usersById, usersById,
chatsById,
canShow, canShow,
isShown, isShown,
isArchived, isArchived,
@ -45,22 +47,22 @@ function StoryToggler({
const lang = useLang(); const lang = useLang();
const users = useMemo(() => { const peers = useMemo(() => {
if (orderedUserIds.length === 1) { if (orderedPeerIds.length === 1) {
return [usersById[orderedUserIds[0]]]; return [usersById[orderedPeerIds[0]] || chatsById[orderedPeerIds[0]]];
} }
return orderedUserIds return orderedPeerIds
.map((id) => usersById[id]) .map((id) => usersById[id] || chatsById[id])
.filter((user) => user && user.id !== currentUserId) .filter((peer) => peer && peer.id !== currentUserId)
.slice(0, PREVIEW_AVATAR_COUNT) .slice(0, PREVIEW_AVATAR_COUNT)
.reverse(); .reverse();
}, [currentUserId, orderedUserIds, usersById]); }, [currentUserId, orderedPeerIds, usersById, chatsById]);
const preloadUserIds = useMemo(() => { const preloadPeerIds = useMemo(() => {
return orderedUserIds.slice(0, PRELOAD_USERS); return orderedPeerIds.slice(0, PRELOAD_PEERS);
}, [orderedUserIds]); }, [orderedPeerIds]);
useStoryPreloader(preloadUserIds); useStoryPreloader(preloadPeerIds);
const isVisible = canShow && isShown; const isVisible = canShow && isShown;
// For some reason, setting 'slow' here also fixes scroll freezes on iOS when collapsing Story Ribbon // For some reason, setting 'slow' here also fixes scroll freezes on iOS when collapsing Story Ribbon
@ -90,10 +92,10 @@ function StoryToggler({
onClick={() => toggleStoryRibbon({ isShown: true, isArchived })} onClick={() => toggleStoryRibbon({ isShown: true, isArchived })}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >
{users.map((user) => ( {peers.map((peer) => (
<Avatar <Avatar
key={user.id} key={peer.id}
peer={user} peer={peer}
size="tiny" size="tiny"
className={styles.avatar} className={styles.avatar}
withStorySolid withStorySolid
@ -104,15 +106,16 @@ function StoryToggler({
} }
export default memo(withGlobal<OwnProps>((global, { isArchived }): StateProps => { export default memo(withGlobal<OwnProps>((global, { isArchived }): StateProps => {
const { orderedUserIds: { archived, active } } = global.stories; const { orderedPeerIds: { archived, active } } = global.stories;
const { storyViewer: { isRibbonShown, isArchivedRibbonShown } } = selectTabState(global); const { storyViewer: { isRibbonShown, isArchivedRibbonShown } } = selectTabState(global);
const withAnimation = selectPerformanceSettingsValue(global, 'storyRibbonAnimations'); const withAnimation = selectPerformanceSettingsValue(global, 'storyRibbonAnimations');
return { return {
currentUserId: global.currentUserId!, currentUserId: global.currentUserId!,
orderedUserIds: isArchived ? archived : active, orderedPeerIds: isArchived ? archived : active,
isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown, isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown,
withAnimation, withAnimation,
usersById: global.users.byId, usersById: global.users.byId,
chatsById: global.chats.byId,
}; };
})(StoryToggler)); })(StoryToggler));

View File

@ -13,8 +13,8 @@ import {
} from '../../config'; } from '../../config';
import { import {
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectPeerStory,
selectTabState, selectTabState,
selectUserStory,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
@ -110,6 +110,7 @@ function StoryViewModal({
const handleLoadMore = useLastCallback(() => { const handleLoadMore = useLastCallback(() => {
if (!story?.id || nextOffset === undefined) return; if (!story?.id || nextOffset === undefined) return;
loadStoryViews({ loadStoryViews({
peerId: story.peerId,
storyId: story.id, storyId: story.id,
offset: nextOffset, offset: nextOffset,
areReactionsFirst: areReactionsFirst || undefined, areReactionsFirst: areReactionsFirst || undefined,
@ -278,7 +279,7 @@ export default memo(withGlobal((global) => {
const { const {
storyId, viewsById, nextOffset, isLoading, storyId, viewsById, nextOffset, isLoading,
} = viewModal || {}; } = viewModal || {};
const story = storyId ? selectUserStory(global, global.currentUserId!, storyId) : undefined; const story = storyId ? selectPeerStory(global, global.currentUserId!, storyId) : undefined;
return { return {
storyId, storyId,

View File

@ -244,7 +244,7 @@
@media (max-width: 600px) { @media (max-width: 600px) {
width: 100% !important; width: 100% !important;
height: calc(100% - 4rem) !important; height: calc(100% - 4rem) !important; // Update `MOBILE_MEDIA_BOTTOM_MARGIN` in MediaAreaOverlay on change
border-radius: 0; border-radius: 0;
} }
@ -276,6 +276,18 @@
} }
} }
.mediaAreaOverlay {
@media (max-width: 600px) {
right: auto;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
max-height: 100%;
}
}
.content { .content {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -644,48 +656,6 @@
right: 0; right: 0;
} }
.ownForward {
position: absolute;
bottom: 0.25rem;
right: 0;
}
.recentViewers {
position: absolute;
bottom: 0;
left: 0;
display: flex;
align-items: center;
transition: background-color 200ms, opacity 350ms !important;
padding: 0.5rem;
border-radius: var(--border-radius-default);
color: #fff;
}
.recentViewersInteractive {
cursor: var(--custom-cursor, pointer);
&:hover {
background-color: rgba(var(--color-text-secondary-rgb), 0.2);
}
}
.recentViewersAvatars {
margin-inline-end: 0.5rem;
}
.reactionCount {
margin-inline-start: 0.5rem;
display: flex;
gap: 0.125rem;
align-items: center;
}
.reactionCountHeart {
color: var(--color-heart);
font-size: 1.25rem;
}
.modal :global(.modal-content) { .modal :global(.modal-content) {
padding: 0.5rem !important; padding: 0.5rem !important;
max-height: 35rem; max-height: 35rem;
@ -705,21 +675,6 @@
object-fit: cover; object-fit: cover;
} }
.mediaAreaOverlay {
position: absolute;
width: auto;
left: 50%;
transform: translateX(-50%);
pointer-events: none;
}
.mediaArea {
position: absolute;
transform-origin: top left;
pointer-events: all;
cursor: var(--custom-cursor, pointer);
}
.ghost { .ghost {
position: absolute; position: absolute;
z-index: 1; z-index: 1;

View File

@ -9,9 +9,9 @@ import type { StoryViewerOrigin } from '../../types';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY } from '../../config';
import { import {
selectIsStoryViewerOpen, selectIsStoryViewerOpen,
selectPeerStory,
selectPerformanceSettingsValue, selectPerformanceSettingsValue,
selectTabState, selectTabState,
selectUserStory,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
@ -41,7 +41,7 @@ const ANIMATION_DURATION = 250;
interface StateProps { interface StateProps {
isOpen: boolean; isOpen: boolean;
userId?: string; peerId: string;
storyId?: number; storyId?: number;
story?: ApiTypeStory; story?: ApiTypeStory;
origin?: StoryViewerOrigin; origin?: StoryViewerOrigin;
@ -52,7 +52,7 @@ interface StateProps {
function StoryViewer({ function StoryViewer({
isOpen, isOpen,
userId, peerId,
storyId, storyId,
story, story,
origin, origin,
@ -63,7 +63,7 @@ function StoryViewer({
const { closeStoryViewer, closeStoryPrivacyEditor } = getActions(); const { closeStoryViewer, closeStoryPrivacyEditor } = getActions();
const lang = useLang(); const lang = useLang();
const [idStoryForDelete, setIdStoryForDelete] = useState<number | undefined>(undefined); const [storyToDelete, setStoryToDelete] = useState<ApiTypeStory | undefined>(undefined);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(false); const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(false);
@ -71,13 +71,13 @@ function StoryViewer({
const slideSizes = useSlideSizes(); const slideSizes = useSlideSizes();
const isPrevOpen = usePrevious(isOpen); const isPrevOpen = usePrevious(isOpen);
const prevBestImageData = usePrevious(bestImageData); const prevBestImageData = usePrevious(bestImageData);
const prevUserId = usePrevious(userId); const prevPeerId = usePrevious(peerId);
const prevOrigin = usePrevious(origin); const prevOrigin = usePrevious(origin);
const isGhostAnimation = Boolean(withAnimation && !shouldSkipHistoryAnimations); const isGhostAnimation = Boolean(withAnimation && !shouldSkipHistoryAnimations);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setIdStoryForDelete(undefined); setStoryToDelete(undefined);
closeReportModal(); closeReportModal();
closeDeleteModal(); closeDeleteModal();
} }
@ -101,14 +101,14 @@ function StoryViewer({
closeStoryViewer(); closeStoryViewer();
}, [closeStoryViewer]); }, [closeStoryViewer]);
const handleOpenDeleteModal = useCallback((id: number) => { const handleOpenDeleteModal = useCallback((s: ApiTypeStory) => {
setIdStoryForDelete(id); setStoryToDelete(s);
openDeleteModal(); openDeleteModal();
}, []); }, []);
const handleCloseDeleteModal = useCallback(() => { const handleCloseDeleteModal = useCallback(() => {
closeDeleteModal(); closeDeleteModal();
setIdStoryForDelete(undefined); setStoryToDelete(undefined);
}, []); }, []);
useEffect(() => (isOpen ? captureEscKeyListener(() => { useEffect(() => (isOpen ? captureEscKeyListener(() => {
@ -116,13 +116,13 @@ function StoryViewer({
}) : undefined), [handleClose, isOpen]); }) : undefined), [handleClose, isOpen]);
useEffect(() => { useEffect(() => {
if (isGhostAnimation && !isPrevOpen && isOpen && userId && thumbnail && origin !== undefined) { if (isGhostAnimation && !isPrevOpen && isOpen && peerId && thumbnail && origin !== undefined) {
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
animateOpening(userId, origin, thumbnail, bestImageData, slideSizes.activeSlide); animateOpening(peerId, origin, thumbnail, bestImageData, slideSizes.activeSlide);
} }
if (isGhostAnimation && isPrevOpen && !isOpen && prevUserId && prevBestImageData && prevOrigin !== undefined) { if (isGhostAnimation && isPrevOpen && !isOpen && prevPeerId && prevBestImageData && prevOrigin !== undefined) {
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
animateClosing(prevUserId, prevOrigin, prevBestImageData); animateClosing(prevPeerId, prevOrigin, prevBestImageData);
} }
}, [ }, [
isGhostAnimation, isGhostAnimation,
@ -132,8 +132,8 @@ function StoryViewer({
isPrevOpen, isPrevOpen,
slideSizes.activeSlide, slideSizes.activeSlide,
thumbnail, thumbnail,
userId, peerId,
prevUserId, prevPeerId,
origin, origin,
prevOrigin, prevOrigin,
]); ]);
@ -169,7 +169,7 @@ function StoryViewer({
<StoryDeleteConfirmModal <StoryDeleteConfirmModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
storyId={idStoryForDelete} story={storyToDelete}
onClose={handleCloseDeleteModal} onClose={handleCloseDeleteModal}
/> />
<StoryViewModal /> <StoryViewModal />
@ -179,7 +179,7 @@ function StoryViewer({
isOpen={isReportModalOpen} isOpen={isReportModalOpen}
onClose={closeReportModal} onClose={closeReportModal}
subject="story" subject="story"
userId={userId} peerId={peerId!}
storyId={storyId} storyId={storyId}
/> />
</ShowTransition> </ShowTransition>
@ -189,16 +189,16 @@ function StoryViewer({
export default memo(withGlobal((global): StateProps => { export default memo(withGlobal((global): StateProps => {
const { const {
shouldSkipHistoryAnimations, storyViewer: { shouldSkipHistoryAnimations, storyViewer: {
storyId, userId, isPrivacyModalOpen, origin, storyId, peerId, isPrivacyModalOpen, origin,
}, },
} = selectTabState(global); } = selectTabState(global);
const story = userId && storyId ? selectUserStory(global, userId, storyId) : undefined; const story = peerId && storyId ? selectPeerStory(global, peerId, storyId) : undefined;
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations'); const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
return { return {
isOpen: selectIsStoryViewerOpen(global), isOpen: selectIsStoryViewerOpen(global),
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
userId, peerId: peerId!,
storyId, storyId,
story, story,
origin, origin,

View File

@ -4,7 +4,7 @@ import { StoryViewerOrigin } from '../../../types';
import { ANIMATION_END_DELAY } from '../../../config'; import { ANIMATION_END_DELAY } from '../../../config';
import fastBlur from '../../../lib/fastBlur'; import fastBlur from '../../../lib/fastBlur';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { getUserStoryHtmlId } from '../../../global/helpers'; import { getPeerStoryHtmlId } from '../../../global/helpers';
import { applyStyles } from '../../../util/animation'; import { applyStyles } from '../../../util/animation';
import stopEvent from '../../../util/stopEvent'; import stopEvent from '../../../util/stopEvent';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment';
@ -207,7 +207,7 @@ function createGhost(source: string, hasBlur = false, isGhost2 = false) {
function getNodes(origin: StoryViewerOrigin, userId: string) { function getNodes(origin: StoryViewerOrigin, userId: string) {
let containerSelector; let containerSelector;
const mediaSelector = `#${getUserStoryHtmlId(userId)}`; const mediaSelector = `#${getPeerStoryHtmlId(userId)}`;
switch (origin) { switch (origin) {
case StoryViewerOrigin.StoryRibbon: case StoryViewerOrigin.StoryRibbon:

Some files were not shown because too many files have changed in this diff Show More