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 CommonProperties<T, U> = {
[K in keyof T & keyof U]: T[K] & U[K];
};
// Fix to make Boolean() work as !!
// https://github.com/microsoft/TypeScript/issues/16655
type Falsy = false | 0 | '' | null | undefined;

View File

@ -51,6 +51,9 @@ function buildApiChatFieldsFromPeerEntity(
const isJoinRequest = Boolean('joinRequest' in peerEntity && peerEntity.joinRequest);
const usernames = buildApiUsernames(peerEntity);
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 {
isMin,
@ -77,6 +80,9 @@ function buildApiChatFieldsFromPeerEntity(
isJoinToSend,
isJoinRequest,
isForum,
areStoriesHidden,
maxStoryId,
hasStories: Boolean(maxStoryId) && !storiesUnavailable,
};
}

View File

@ -471,9 +471,9 @@ export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessag
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 {
@ -564,14 +564,14 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
const attributeStory = attributes
?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
if (attributeStory) {
const userId = buildApiPeerId(attributeStory.userId, 'user');
const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = {
id: attributeStory.id,
userId,
peerId,
};
if (attributeStory.story instanceof GramJs.StoryItem) {
addStoryToLocalDb(attributeStory.story, userId);
addStoryToLocalDb(attributeStory.story, peerId);
}
}

View File

@ -11,6 +11,7 @@ import type {
ApiMessageEntity,
ApiMessageForwardInfo,
ApiNewPoll,
ApiPeer,
ApiPhoto,
ApiReplyKeyboard,
ApiSponsoredMessage,
@ -19,7 +20,6 @@ import type {
ApiStorySkipped,
ApiThreadInfo,
ApiTypeReplyTo,
ApiUser,
ApiVideo,
PhoneCallAction,
} from '../../types';
@ -690,7 +690,7 @@ export function buildLocalMessage(
contact?: ApiContact,
groupedId?: string,
scheduledAt?: number,
sendAs?: ApiChat | ApiUser,
sendAs?: ApiPeer,
story?: ApiStory | ApiStorySkipped,
): ApiMessage {
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,
} = form;
const {
test, currency, prices, recurring, recurringTermsUrl, maxTipAmount, suggestedTipAmounts,
test, currency, prices, recurring, termsUrl, maxTipAmount, suggestedTipAmounts,
} = invoice;
const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0);
@ -159,7 +159,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI
currency,
isTest: test,
isRecurring: recurring,
recurringTermsUrl,
termsUrl,
maxTipAmount: maxTipAmount?.toJSNumber(),
...(suggestedTipAmounts && { suggestedTipAmounts: suggestedTipAmounts.map((tip) => tip.toJSNumber()) }),
};

View File

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

View File

@ -7,14 +7,14 @@ import type {
import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildPrivacyRules } from './common';
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiPeerId } from './peers';
import { buildApiReaction } from './reactions';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
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) {
return {
id: story.id,
userId,
peerId,
isDeleted: true,
};
}
@ -26,7 +26,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
return {
id,
userId,
peerId,
...(closeFriends && { isForCloseFriends: true }),
date,
expireDate,
@ -37,7 +37,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
edited, pinned, expireDate, id, date, caption,
entities, media, privacy, views,
public: isPublic, noforwards, closeFriends, contacts, selectedContacts,
mediaAreas, sentReaction,
mediaAreas, sentReaction, out,
} = story;
const content: ApiMessage['content'] = {
@ -50,7 +50,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
return {
id,
userId,
peerId,
date,
expireDate,
content,
@ -63,9 +63,11 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT
...(noforwards && { noForwards: true }),
...(views?.viewsCount && { viewsCount: views.viewsCount }),
...(views?.reactionsCount && { reactionsCount: views.reactionsCount }),
...(views?.reactions && { reactions: views.reactions.map(buildReactionCount) }),
...(views?.recentViewers && {
recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')),
}),
...(out && { isOut: true }),
...(privacy && { visibility: buildPrivacyRules(privacy) }),
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
...(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;
}
export function buildApiUsersStories(userStories: GramJs.UserStories) {
const userId = buildApiPeerId(userStories.userId, 'user');
export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
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) {
const user = localDb.users[story.userId];
const peer = buildInputPeerFromLocalDb(story.peerId)!;
return new GramJs.InputMediaStory({
userId: new GramJs.InputUser({ userId: BigInt(user!.id), accessHash: user!.accessHash! }),
peer,
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)) {
return;
}
const storyData = {
id: story.id,
userId,
peerId,
};
if (story.media instanceof GramJs.MessageMediaPhoto) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import type {
ApiMessageSearchType,
ApiNewPoll,
ApiOnProgress,
ApiPeer,
ApiPoll,
ApiReportReason,
ApiSendMessageAction,
@ -18,7 +19,6 @@ import type {
ApiStory,
ApiStorySkipped,
ApiTypeReplyTo,
ApiUser,
ApiVideo,
OnApiUpdate,
} from '../../types';
@ -267,7 +267,7 @@ export function sendMessage(
scheduledAt?: number;
groupedId?: string;
noWebPage?: boolean;
sendAs?: ApiUser | ApiChat;
sendAs?: ApiPeer;
shouldUpdateStickerSetOrder?: boolean;
},
onProgress?: ApiOnProgress,
@ -417,7 +417,7 @@ function sendGroupedMedia(
groupedId: string;
isSilent?: boolean;
scheduledAt?: number;
sendAs?: ApiUser | ApiChat;
sendAs?: ApiPeer;
},
randomId: GramJs.long,
localMessage: ApiMessage,
@ -785,7 +785,7 @@ export async function deleteHistory({
export async function reportMessages({
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({
peer: buildInputPeer(peer.id, peer.accessHash),
@ -800,7 +800,7 @@ export async function reportMessages({
export async function sendMessageAction({
peer, threadId, action,
}: {
peer: ApiChat | ApiUser; threadId?: number; action: ApiSendMessageAction;
peer: ApiPeer; threadId?: number; action: ApiSendMessageAction;
}) {
const gramAction = buildSendMessageAction(action);
if (!gramAction) {
@ -1299,7 +1299,7 @@ export async function forwardMessages({
messages: ApiMessage[];
isSilent?: boolean;
scheduledAt?: number;
sendAs?: ApiUser | ApiChat;
sendAs?: ApiPeer;
withMyScore?: boolean;
noAuthors?: boolean;
noCaptions?: boolean;
@ -1517,7 +1517,7 @@ export async function fetchSendAs({
export function saveDefaultSendAs({
sendAs, chat,
}: {
sendAs: ApiChat | ApiUser; chat: ApiChat;
sendAs: ApiPeer; chat: ApiChat;
}) {
return invokeRequest(new GramJs.messages.SaveDefaultSendAs({
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 {
ApiReaction, ApiReportReason, ApiStealthMode,
ApiTypeStory, ApiUser, ApiUserStories,
ApiChat,
ApiPeer,
ApiPeerStories,
ApiReaction,
ApiReportReason,
ApiStealthMode,
ApiTypeStory,
ApiUser,
} from '../../types';
import { STORY_LIST_LIMIT } from '../../../config';
import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildApiPeerId } from '../apiBuilders/peers';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import {
buildApiPeerStories,
buildApiStealthMode,
buildApiStory, buildApiStoryView, buildApiUsersStories,
buildApiStory,
buildApiStoryView,
} from '../apiBuilders/stories';
import { buildApiUser } from '../apiBuilders/users';
import {
buildInputEntity,
buildInputPeer,
buildInputPeerFromLocalDb,
buildInputPrivacyRules,
buildInputReaction,
buildInputReportReason,
@ -38,7 +45,8 @@ export async function fetchAllStories({
| { state: string; stealthMode: ApiStealthMode }
| {
users: ApiUser[];
userStories: Record<string, ApiUserStories>;
chats: ApiChat[];
peerStories: Record<string, ApiPeerStories>;
hasMore?: true;
state: string;
stealthMode: ApiStealthMode;
@ -60,13 +68,14 @@ export async function fetchAllStories({
}
addEntitiesToLocalDb(result.users);
result.userStories.forEach((userStories) => (
userStories.stories.forEach((story) => addStoryToLocalDb(story, buildApiPeerId(userStories.userId, 'user')))
addEntitiesToLocalDb(result.chats);
result.peerStories.forEach((peerStories) => (
peerStories.stories.forEach((story) => addStoryToLocalDb(story, getApiChatIdFromMtpPeer(peerStories.peer)))
));
const allUserStories = result.userStories.reduce<Record<string, ApiUserStories>>((acc, userStories) => {
const userId = buildApiPeerId(userStories.userId, 'user');
const stories = buildApiUsersStories(userStories);
const allUserStories = result.peerStories.reduce<Record<string, ApiPeerStories>>((acc, peerStories) => {
const peerId = getApiChatIdFromMtpPeer(peerStories.peer);
const stories = buildApiPeerStories(peerStories);
const { pinnedIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce<
{
pinnedIds: number[];
@ -93,12 +102,12 @@ export async function fetchAllStories({
return acc;
}
acc[userId] = {
acc[peerId] = {
byId: stories,
orderedIds,
pinnedIds,
lastUpdatedAt,
lastReadId: userStories.maxReadId,
lastReadId: peerStories.maxReadId,
};
return acc;
@ -106,20 +115,21 @@ export async function fetchAllStories({
return {
users: result.users.map(buildApiUser).filter(Boolean),
userStories: allUserStories,
chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
peerStories: allUserStories,
hasMore: result.hasMore,
state: result.state,
stealthMode: buildApiStealthMode(result.stealthMode),
};
}
export async function fetchUserStories({
user,
export async function fetchPeerStories({
peer,
}: {
user: ApiUser;
peer: ApiPeer;
}) {
const result = await invokeRequest(new GramJs.stories.GetUserStories({
userId: buildInputPeer(user.id, user.accessHash),
const result = await invokeRequest(new GramJs.stories.GetPeerStories({
peer: buildInputPeer(peer.id, peer.accessHash),
}));
if (!result) {
@ -127,55 +137,58 @@ export async function fetchUserStories({
}
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 chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const stories = buildCollectionByCallback(result.stories.stories, (story) => (
[story.id, buildApiStory(user.id, story)]
[story.id, buildApiStory(peer.id, story)]
));
return {
chats,
users,
stories,
lastReadStoryId: result.stories.maxReadId,
};
}
export function fetchUserPinnedStories({
user, offsetId,
export function fetchPeerPinnedStories({
peer, offsetId,
}: {
user: ApiUser;
peer: ApiPeer;
offsetId?: number;
}) {
return fetchCommonStoriesRequest({
method: new GramJs.stories.GetPinnedStories({
userId: buildInputPeer(user.id, user.accessHash),
peer: buildInputPeer(peer.id, peer.accessHash),
offsetId,
limit: STORY_LIST_LIMIT,
}),
userId: user.id,
peerId: peer.id,
});
}
export function fetchStoriesArchive({
currentUserId,
peer,
offsetId,
}: {
currentUserId: string;
peer: ApiPeer;
offsetId?: number;
}) {
return fetchCommonStoriesRequest({
method: new GramJs.stories.GetStoriesArchive({
peer: peer && buildInputPeer(peer.id, peer.accessHash),
offsetId,
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({
userId: buildInputPeer(user.id, user.accessHash),
peer: buildInputPeer(peer.id, peer.accessHash),
id: ids,
}));
@ -184,17 +197,19 @@ export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids:
}
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 chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const stories = ids.reduce<Record<string, ApiTypeStory>>((acc, id) => {
const story = result.stories.find(({ id: currentId }) => currentId === id);
if (story) {
acc[id] = buildApiStory(user.id, story);
acc[id] = buildApiStory(peer.id, story);
} else {
acc[id] = {
id,
userId: user.id,
peerId: peer.id,
isDeleted: true,
};
}
@ -203,34 +218,43 @@ export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids:
}, {});
return {
chats,
users,
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({
userId: buildInputPeer(user.id, user.accessHash),
peer: buildInputPeer(peer.id, peer.accessHash),
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({
userId: buildInputPeer(user.id, user.accessHash),
peer: buildInputPeer(peer.id, peer.accessHash),
maxId: storyId,
}));
}
export function deleteStory({ storyId }: { storyId: number }) {
return invokeRequest(new GramJs.stories.DeleteStories({ id: [storyId] }));
export function deleteStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
return invokeRequest(new GramJs.stories.DeleteStories({
peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId],
}));
}
export function toggleStoryPinned({ storyId, isPinned }: { storyId: number; isPinned?: boolean }) {
return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned }));
export function toggleStoryPinned({ peer, storyId, isPinned }: { peer: ApiPeer; storyId: number; isPinned?: boolean }) {
return invokeRequest(new GramJs.stories.TogglePinned({
peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId],
pinned: isPinned,
}));
}
export async function fetchStoryViewList({
peer,
storyId,
areJustContacts,
query,
@ -238,6 +262,7 @@ export async function fetchStoryViewList({
limit = STORY_LIST_LIMIT,
offset = '',
}: {
peer: ApiPeer;
storyId: number;
areJustContacts?: true;
areReactionsFirst?: true;
@ -246,6 +271,7 @@ export async function fetchStoryViewList({
offset?: string;
}) {
const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({
peer: buildInputPeer(peer.id, peer.accessHash),
id: storyId,
justContacts: areJustContacts,
q: query,
@ -271,14 +297,9 @@ export async function fetchStoryViewList({
};
}
export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) {
const inputUser = buildInputPeerFromLocalDb(userId);
if (!inputUser) {
return undefined;
}
export async function fetchStoryLink({ peer, storyId }: { peer: ApiPeer ; storyId: number }) {
const result = await invokeRequest(new GramJs.stories.ExportStoryLink({
userId: inputUser,
peer: buildInputPeer(peer.id, peer.accessHash),
id: storyId,
}));
@ -290,15 +311,15 @@ export async function fetchStoryLink({ userId, storyId }: { userId: string; stor
}
export function reportStory({
user,
peer,
storyId,
reason,
description,
}: {
user: ApiUser; storyId: number; reason: ApiReportReason; description?: string;
peer: ApiPeer; storyId: number; reason: ApiReportReason; description?: string;
}) {
return invokeRequest(new GramJs.stories.Report({
userId: buildInputPeer(user.id, user.accessHash),
peer: buildInputPeer(peer.id, peer.accessHash),
id: [storyId],
reason: buildInputReportReason(reason),
message: description,
@ -306,13 +327,16 @@ export function reportStory({
}
export function editStoryPrivacy({
peer,
id,
privacy,
}: {
peer: ApiPeer;
id: number;
privacy: ApiInputPrivacyRules;
}) {
return invokeRequest(new GramJs.stories.EditStory({
peer: buildInputPeer(peer.id, peer.accessHash),
id,
privacyRules: buildInputPrivacyRules(privacy),
}), {
@ -321,31 +345,31 @@ export function editStoryPrivacy({
}
export function toggleStoriesHidden({
user,
peer,
isHidden,
}: {
user: ApiUser;
peer: ApiPeer;
isHidden: boolean;
}) {
return invokeRequest(new GramJs.contacts.ToggleStoriesHidden({
id: buildInputPeer(user.id, user.accessHash),
return invokeRequest(new GramJs.stories.TogglePeerStoriesHidden({
peer: buildInputPeer(peer.id, peer.accessHash),
hidden: isHidden,
}));
}
export function fetchStoriesMaxIds({
users,
peers,
}: {
users: ApiUser[];
peers: ApiPeer[];
}) {
return invokeRequest(new GramJs.users.GetStoriesMaxIDs({
id: users.map((user) => buildInputPeer(user.id, user.accessHash)),
return invokeRequest(new GramJs.stories.GetPeerMaxIDs({
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;
userId: string;
peerId: string;
}) {
const result = await invokeRequest(method);
@ -354,30 +378,33 @@ async function fetchCommonStoriesRequest({ method, userId }: {
}
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 chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const stories = buildCollectionByCallback(result.stories, (story) => (
[story.id, buildApiStory(userId, story)]
[story.id, buildApiStory(peerId, story)]
));
return {
users,
chats,
stories,
};
}
export function sendStoryReaction({
user, storyId, reaction, shouldAddToRecent,
peer, storyId, reaction, shouldAddToRecent,
}: {
user: ApiUser;
peer: ApiPeer;
storyId: number;
reaction?: ApiReaction;
shouldAddToRecent?: boolean;
}) {
return invokeRequest(new GramJs.stories.SendReaction({
reaction: reaction ? buildInputReaction(reaction) : new GramJs.ReactionEmpty(),
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
peer: buildInputPeer(peer.id, peer.accessHash),
storyId,
...(shouldAddToRecent && { addToRecent: true }),
}), {

View File

@ -2,7 +2,7 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiChat, ApiSticker,
ApiChat, ApiPeer, ApiSticker,
ApiUser, OnApiUpdate,
} 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;
return invokeRequest(new GramJs.messages.ReportSpam({

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import React, {
} from '../../../lib/teact/teact';
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 { GROUP_CALL_DEFAULT_VOLUME } from '../../../config';
@ -31,7 +31,7 @@ type OwnProps = {
};
type StateProps = {
peer?: ApiUser | ApiChat;
peer?: ApiPeer;
};
const GroupCallParticipant: FC<OwnProps & StateProps> = ({

View File

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

View File

@ -1,7 +1,7 @@
import type { FC } 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 buildClassName from '../../util/buildClassName';
@ -14,7 +14,7 @@ import styles from './AvatarList.module.scss';
type OwnProps = {
size: AvatarSize;
peers?: (ApiUser | ApiChat)[];
peers?: ApiPeer[];
className?: string;
};

View File

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

View File

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

View File

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

View File

@ -41,6 +41,7 @@ type OwnProps = {
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
onClick?: NoneToVoidFunction;
onAnimationEnd?: NoneToVoidFunction;
};
const STICKER_SIZE = 20;
@ -65,6 +66,7 @@ const CustomEmoji: FC<OwnProps> = ({
observeIntersectionForLoading,
observeIntersectionForPlaying,
onClick,
onAnimationEnd,
}) => {
// eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null);
@ -119,6 +121,7 @@ const CustomEmoji: FC<OwnProps> = ({
withGridFix && styles.withGridFix,
)}
onClick={onClick}
onAnimationEnd={onAnimationEnd}
data-entity-type={ApiMessageEntityTypes.CustomEmoji}
data-document-id={documentId}
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 type {
ApiChat,
ApiMessage, ApiUser,
ApiMessage, ApiPeer,
} from '../../api/types';
import type { ChatTranslatedMessages } from '../../global/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
@ -37,7 +36,7 @@ import './EmbeddedMessage.scss';
type OwnProps = {
className?: string;
message?: ApiMessage;
sender?: ApiUser | ApiChat;
sender?: ApiPeer;
title?: string;
customText?: string;
noUserColors?: boolean;

View File

@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { useRef } from '../../lib/teact/teact';
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 {
@ -24,7 +24,7 @@ import './EmbeddedMessage.scss';
type OwnProps = {
story?: ApiTypeStory;
sender?: ApiUser | ApiChat;
sender?: ApiPeer;
noUserColors?: boolean;
isProtected?: boolean;
observeIntersectionForLoading?: ObserveFn;

View File

@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
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 { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
@ -23,7 +23,7 @@ import VerifiedIcon from './VerifiedIcon';
import styles from './FullNameTitle.module.scss';
type OwnProps = {
peer: ApiChat | ApiUser;
peer: ApiPeer;
className?: string;
noVerified?: boolean;
noFake?: boolean;

View File

@ -6,7 +6,7 @@ import type {
ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus,
} from '../../api/types';
import type { LangFn } from '../../hooks/useLang';
import { MediaViewerOrigin } from '../../types';
import { MediaViewerOrigin, type StoryViewerOrigin } from '../../types';
import {
getChatTypeString,
@ -51,6 +51,8 @@ type OwnProps = {
noRtl?: boolean;
noAvatar?: boolean;
noStatusOrTyping?: boolean;
withStory?: boolean;
storyViewerOrigin?: StoryViewerOrigin;
onClick?: VoidFunction;
};
@ -84,6 +86,8 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
topic,
messagesCount,
noStatusOrTyping,
withStory,
storyViewerOrigin,
onClick,
}) => {
const {
@ -186,6 +190,9 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
key={chat.id}
size={avatarSize}
peer={chat}
withStory={withStory}
storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer"
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 { IconName } from '../../types/icons';
import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers';
import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
import { selectChat, selectUser } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText';
@ -111,8 +111,8 @@ export default memo(withGlobal<OwnProps>(
return {};
}
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
const chat = selectChat(global, chatOrUserId);
const user = selectUser(global, chatOrUserId);
const isSavedMessages = !forceShowSelf && user && user.isSelf;
return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,11 @@ import React, {
} from '../../../lib/teact/teact';
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 {
selectCanAnimateInterface,
selectChat,
selectChatMessage,
selectCurrentMessageList,
selectEditingId,
@ -18,10 +17,10 @@ import {
selectForwardedSender,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectPeer,
selectReplyingToId,
selectSender,
selectTabState,
selectUser,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
@ -45,7 +44,7 @@ type StateProps = {
replyingToId?: number;
editingId?: number;
message?: ApiMessage;
sender?: ApiUser | ApiChat;
sender?: ApiPeer;
shouldAnimate?: boolean;
forwardedMessagesCount?: number;
noAuthors?: boolean;
@ -313,7 +312,7 @@ export default memo(withGlobal<OwnProps>(
message = forwardedMessages?.[0];
}
let sender: ApiChat | ApiUser | undefined;
let sender: ApiPeer | undefined;
if (replyingToId && message && !shouldForceShowEditing) {
const { forwardInfo } = message;
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
@ -332,7 +331,7 @@ export default memo(withGlobal<OwnProps>(
}
}
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 useShowTransition from '../../../hooks/useShowTransition';
import MediaAreaOverlay from '../../story/mediaArea/MediaAreaOverlay';
import styles from './BaseStory.module.scss';
interface OwnProps {
@ -28,7 +30,7 @@ interface OwnProps {
function BaseStory({
story, isPreview, isProtected, isConnected,
}: OwnProps) {
const { openStoryViewer, loadUserStoriesByIds, showNotification } = getActions();
const { openStoryViewer, loadPeerStoriesByIds, showNotification } = getActions();
const lang = useLang();
const { isMobile } = useAppLayout();
@ -56,7 +58,7 @@ function BaseStory({
useEffect(() => {
if (story && !(isLoaded || isExpired)) {
loadUserStoriesByIds({ userId: story.userId, storyIds: [story.id] });
loadPeerStoriesByIds({ peerId: story.peerId, storyIds: [story.id] });
}
}, [story, isExpired, isLoaded]);
@ -69,9 +71,9 @@ function BaseStory({
}
openStoryViewer({
userId: story!.userId,
peerId: story!.peerId,
storyId: story!.id,
isSingleUser: true,
isSinglePeer: true,
isSingleStory: true,
});
});
@ -83,12 +85,15 @@ function BaseStory({
>
{!isExpired && isPreview && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />}
{shouldRender && (
<img
src={mediaUrl}
alt=""
className={buildClassName(styles.media, isPreview && styles.linkPreview, transitionClassNames)}
draggable={false}
/>
<>
<img
src={mediaUrl}
alt=""
className={buildClassName(styles.media, isPreview && styles.linkPreview, transitionClassNames)}
draggable={false}
/>
{isLoaded && <MediaAreaOverlay story={story} className={transitionClassNames} />}
</>
)}
{isExpired && (
<span>

View File

@ -6,7 +6,7 @@ import type {
ApiThreadInfo,
} from '../../../api/types';
import { isUserId } from '../../../global/helpers';
import { selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
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
const { users: { byId: usersById }, chats: { byId: chatsById } } = getGlobal();
const global = getGlobal();
return recentReplierIds.map((peerId) => {
return isUserId(peerId) ? usersById[peerId] : chatsById[peerId];
return selectPeer(global, peerId);
}).filter(Boolean);
}, [recentReplierIds]);

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiChat, ApiMessage, ApiPoll, ApiPollAnswer, ApiUser,
ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer,
} from '../../../api/types';
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
const chatsById = getGlobal().chats.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 user = usersById[id];
if (user) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,17 +35,17 @@ import {
selectChatMessages,
selectCurrentMediaSearch,
selectIsRightColumnShown,
selectPeerFullInfo,
selectPeerStories,
selectTabState,
selectTheme,
selectUser,
selectUserFullInfo,
selectUserStories,
} from '../../global/selectors';
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
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 useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
@ -170,7 +170,7 @@ const Profile: FC<OwnProps & StateProps> = ({
focusMessage,
loadProfilePhotos,
setNewChatMembersDialogState,
loadUserPinnedStories,
loadPeerPinnedStories,
loadStoriesArchive,
} = getActions();
@ -215,18 +215,18 @@ const Profile: FC<OwnProps & StateProps> = ({
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
const handleLoadUserStories = useCallback(({ offsetId }: { offsetId: number }) => {
loadUserPinnedStories({ userId: chatId, offsetId });
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
loadPeerPinnedStories({ peerId: chatId, offsetId });
}, [chatId]);
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
loadStoriesArchive({ offsetId });
}, []);
loadStoriesArchive({ peerId: currentUserId!, offsetId });
}, [currentUserId]);
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
loadMoreMembers,
loadCommonChats,
searchMediaMessagesLocal,
handleLoadUserStories,
handleLoadPeerStories,
handleLoadStoriesArchive,
tabType,
mediaSearchType,
@ -246,7 +246,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|| (!hasMembersTab && resultType === 'media');
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);
@ -606,24 +606,22 @@ export default memo(withGlobal<OwnProps>(
const activeDownloads = selectActiveDownloads(global, chatId);
let hasCommonChatsTab;
let hasStoriesTab;
let resolvedUserId;
let user;
let storyIds;
let archiveStoryIds;
let storyByIds;
if (isUserId(chatId)) {
resolvedUserId = chatId;
user = selectUser(global, resolvedUserId);
const userFullInfo = selectUserFullInfo(global, chatId);
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 {
theme: selectTheme(global),
isChannel,

View File

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

View File

@ -29,9 +29,9 @@ type OwnProps = {
selectedUserId?: string;
isPromotedByCurrentUser?: boolean;
isNewAdmin?: boolean;
isActive: boolean;
onScreenSelect: (screen: ManagementScreens) => void;
onClose: NoneToVoidFunction;
isActive: boolean;
};
type StateProps = {
@ -49,10 +49,10 @@ type StateProps = {
const CUSTOM_TITLE_MAX_LENGTH = 16;
const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
isActive,
isNewAdmin,
selectedUserId,
defaultRights,
onScreenSelect,
chat,
usersById,
currentUserId,
@ -62,7 +62,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
isForum,
isFormFullyDisabled,
onClose,
isActive,
onScreenSelect,
}) => {
const { updateChatAdmin } = getActions();
@ -260,6 +260,42 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
onChange={handlePermissionChange}
/>
</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 && (
<div className="ListItem">
<Checkbox

View File

@ -14,6 +14,7 @@ import {
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees';
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
@ -105,6 +106,8 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds;
}, [members, userStatusesById, noAdmins, adminIds]);
usePeerStoriesPolling(memberIds);
const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId;
@ -223,7 +226,7 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
onClick={() => handleMemberClick(id)}
contextActions={getMemberContextAction(id)}
>
<PrivateChatInfo userId={id} forceShowSelf />
<PrivateChatInfo userId={id} forceShowSelf withStory />
</ListItem>
))}
</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 MenuItem from '../ui/MenuItem';
import MediaAreaOverlay from './mediaArea/MediaAreaOverlay';
import styles from './MediaStory.module.scss';
@ -30,7 +31,7 @@ interface OwnProps {
function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const {
openStoryViewer,
loadUserSkippedStories,
loadPeerSkippedStories,
toggleStoryPinned,
showNotification,
} = getActions();
@ -44,6 +45,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const getMenuElement = useLastCallback(() => document.querySelector('#portals .story-context-menu .bubble'));
const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true }));
const peerId = story && story.peerId;
const isFullyLoaded = story && 'content' in story;
const isDeleted = story && 'isDeleted' in story;
const video = isFullyLoaded ? (story as ApiStory).content.video : undefined;
@ -53,7 +55,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
useEffect(() => {
if (story && !(isFullyLoaded || isDeleted)) {
loadUserSkippedStories({ userId: story.userId });
loadPeerSkippedStories({ peerId: story.peerId });
}
}, [isDeleted, isFullyLoaded, story]);
@ -74,13 +76,13 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const handleClick = useCallback(() => {
openStoryViewer({
userId: story.userId,
peerId: story.peerId,
storyId: story.id,
isSingleUser: true,
isSinglePeer: true,
isPrivate: true,
isArchive,
});
}, [isArchive, story.id, story.userId]);
}, [isArchive, story.id, story.peerId]);
const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
preventMessageInputBlurWithBubbling(e);
@ -90,7 +92,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const handlePinClick = useLastCallback((e: React.SyntheticEvent) => {
stopEvent(e);
toggleStoryPinned({ storyId: story.id, isPinned: true });
toggleStoryPinned({ peerId, storyId: story.id, isPinned: true });
showNotification({
message: lang('Story.ToastSavedToProfileText'),
});
@ -100,7 +102,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
const handleUnpinClick = useLastCallback((e: React.SyntheticEvent) => {
stopEvent(e);
toggleStoryPinned({ storyId: story.id, isPinned: false });
toggleStoryPinned({ peerId, storyId: story.id, isPinned: false });
showNotification({
message: lang('Story.ToastRemovedFromProfileText'),
});
@ -125,6 +127,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) {
{thumbUrl && (
<img src={thumbUrl} alt="" className={styles.media} draggable={false} />
)}
{isFullyLoaded && <MediaAreaOverlay story={story} />}
{isProtected && <span className="protector" />}
</div>
{contextMenuPosition !== undefined && (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import React, { memo, useRef } from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { ApiUser } from '../../api/types';
import type { ApiPeer } from '../../api/types';
import { StoryViewerOrigin } from '../../types';
import { getUserFirstOrLastName } from '../../global/helpers';
import { getSenderTitle, isUserId } from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
@ -21,11 +21,11 @@ import MenuItem from '../ui/MenuItem';
import styles from './StoryRibbon.module.scss';
interface OwnProps {
user: ApiUser;
peer: ApiPeer;
isArchived?: boolean;
}
function StoryRibbonButton({ user, isArchived }: OwnProps) {
function StoryRibbonButton({ peer, isArchived }: OwnProps) {
const {
openChat,
openChatWithInfo,
@ -37,7 +37,10 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
// eslint-disable-next-line no-null/no-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 {
isContextMenuOpen, contextMenuPosition,
@ -47,7 +50,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
const getTriggerElement = useLastCallback(() => ref.current);
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 {
@ -63,7 +66,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
const handleClick = useLastCallback(() => {
if (isContextMenuOpen) return;
openStoryViewer({ userId: user.id, origin: StoryViewerOrigin.StoryRibbon });
openStoryViewer({ peerId: peer.id, origin: StoryViewerOrigin.StoryRibbon });
});
const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
@ -72,44 +75,44 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
});
const handleSavedStories = useLastCallback(() => {
openChatWithInfo({ id: user.id, shouldReplaceHistory: true, profileTab: 'stories' });
openChatWithInfo({ id: peer.id, shouldReplaceHistory: true, profileTab: 'stories' });
});
const handleArchivedStories = useLastCallback(() => {
openChatWithInfo({ id: user.id, shouldReplaceHistory: true, profileTab: 'storiesArchive' });
openChatWithInfo({ id: peer.id, shouldReplaceHistory: true, profileTab: 'storiesArchive' });
});
const handleOpenChat = useLastCallback(() => {
openChat({ id: user.id, shouldReplaceHistory: true });
openChat({ id: peer.id, shouldReplaceHistory: true });
});
const handleOpenProfile = useLastCallback(() => {
openChatWithInfo({ id: user.id, shouldReplaceHistory: true });
openChatWithInfo({ id: peer.id, shouldReplaceHistory: true });
});
const handleArchiveUser = useLastCallback(() => {
toggleStoriesHidden({ userId: user.id, isHidden: !isArchived });
const handleArchivePeer = useLastCallback(() => {
toggleStoriesHidden({ peerId: peer.id, isHidden: !isArchived });
});
return (
<div
ref={ref}
role="button"
data-peer-id={user.id}
data-peer-id={peer.id}
tabIndex={0}
className={styles.user}
className={styles.peer}
onMouseDown={handleMouseDown}
onClick={handleClick}
onContextMenu={handleContextMenu}
>
<Avatar
peer={user}
peer={peer}
withStory
storyViewerOrigin={StoryViewerOrigin.StoryRibbon}
storyViewerMode="full"
/>
<div className={buildClassName(styles.name, user.hasUnreadStories && styles.name_hasUnreadStory)}>
{user.isSelf ? lang('MyStory') : getUserFirstOrLastName(user)}
<div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}>
{isSelf ? lang('MyStory') : getSenderTitle(lang, peer)}
</div>
{contextMenuPosition !== undefined && (
<Menu
@ -119,13 +122,13 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
positionX={positionX}
positionY={positionY}
style={menuStyle}
className={buildClassName('story-user-context-menu', styles.contextMenu)}
className={buildClassName('story-peer-context-menu', styles.contextMenu)}
autoClose
withPortal
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
>
{user.isSelf ? (
{isSelf ? (
<>
<MenuItem onClick={handleSavedStories} icon="play-story">
{lang('StoryList.Context.SavedStories')}
@ -136,14 +139,22 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
</>
) : (
<>
<MenuItem onClick={handleOpenChat} icon="message">
{lang('SendMessageTitle')}
</MenuItem>
<MenuItem onClick={handleOpenProfile} icon="user">
{lang('StoryList.Context.ViewProfile')}
</MenuItem>
{!isChannel && (
<MenuItem onClick={handleOpenChat} icon="message">
{lang('SendMessageTitle')}
</MenuItem>
)}
{isChannel ? (
<MenuItem onClick={handleOpenProfile} icon="channel">
{lang('ChatList.ContextOpenChannel')}
</MenuItem>
) : (
<MenuItem onClick={handleOpenProfile} icon="user">
{lang('StoryList.Context.ViewProfile')}
</MenuItem>
)}
<MenuItem
onClick={handleArchiveUser}
onClick={handleArchivePeer}
icon={isArchived ? 'unarchive' : '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 { IconName } from '../../types/icons';
import { getUserFullName } from '../../global/helpers';
import { selectTabState, selectUserStory } from '../../global/selectors';
import { getSenderTitle, getUserFullName } from '../../global/helpers';
import { selectPeerStory, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import stopEvent from '../../util/stopEvent';
@ -191,11 +191,12 @@ function StorySettings({
const handleSubmit = useLastCallback(() => {
editStoryPrivacy({
peerId: story!.peerId,
storyId: story!.id,
privacy: privacy!,
});
if (story!.isPinned !== isPinned) {
toggleStoryPinned({ storyId: story!.id, isPinned });
toggleStoryPinned({ peerId: story!.peerId, storyId: story!.id, isPinned });
}
closeModal();
});
@ -207,7 +208,7 @@ function StorySettings({
}
if (closeFriendIds.length === 1) {
return getUserFullName(usersById[closeFriendIds[0]]);
return getSenderTitle(lang, usersById[closeFriendIds[0]]);
}
return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i');
@ -401,11 +402,11 @@ function StorySettings({
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
storyViewer: {
storyId, userId,
storyId, peerId,
},
} = selectTabState(global);
const story = (userId && storyId)
? selectUserStory(global, userId, storyId)
const story = (peerId && storyId)
? selectPeerStory(global, peerId, storyId)
: undefined;
return {

View File

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

View File

@ -1,7 +1,7 @@
import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
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 { selectPerformanceSettingsValue, selectTabState } from '../../global/selectors';
@ -24,18 +24,20 @@ interface OwnProps {
interface StateProps {
currentUserId: string;
orderedUserIds: string[];
orderedPeerIds: string[];
isShown: boolean;
withAnimation?: boolean;
usersById: Record<string, ApiUser>;
chatsById: Record<string, ApiChat>;
}
const PRELOAD_USERS = 5;
const PRELOAD_PEERS = 5;
function StoryToggler({
currentUserId,
orderedUserIds,
orderedPeerIds,
usersById,
chatsById,
canShow,
isShown,
isArchived,
@ -45,22 +47,22 @@ function StoryToggler({
const lang = useLang();
const users = useMemo(() => {
if (orderedUserIds.length === 1) {
return [usersById[orderedUserIds[0]]];
const peers = useMemo(() => {
if (orderedPeerIds.length === 1) {
return [usersById[orderedPeerIds[0]] || chatsById[orderedPeerIds[0]]];
}
return orderedUserIds
.map((id) => usersById[id])
.filter((user) => user && user.id !== currentUserId)
return orderedPeerIds
.map((id) => usersById[id] || chatsById[id])
.filter((peer) => peer && peer.id !== currentUserId)
.slice(0, PREVIEW_AVATAR_COUNT)
.reverse();
}, [currentUserId, orderedUserIds, usersById]);
}, [currentUserId, orderedPeerIds, usersById, chatsById]);
const preloadUserIds = useMemo(() => {
return orderedUserIds.slice(0, PRELOAD_USERS);
}, [orderedUserIds]);
useStoryPreloader(preloadUserIds);
const preloadPeerIds = useMemo(() => {
return orderedPeerIds.slice(0, PRELOAD_PEERS);
}, [orderedPeerIds]);
useStoryPreloader(preloadPeerIds);
const isVisible = canShow && isShown;
// 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 })}
dir={lang.isRtl ? 'rtl' : undefined}
>
{users.map((user) => (
{peers.map((peer) => (
<Avatar
key={user.id}
peer={user}
key={peer.id}
peer={peer}
size="tiny"
className={styles.avatar}
withStorySolid
@ -104,15 +106,16 @@ function StoryToggler({
}
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 withAnimation = selectPerformanceSettingsValue(global, 'storyRibbonAnimations');
return {
currentUserId: global.currentUserId!,
orderedUserIds: isArchived ? archived : active,
orderedPeerIds: isArchived ? archived : active,
isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown,
withAnimation,
usersById: global.users.byId,
chatsById: global.chats.byId,
};
})(StoryToggler));

View File

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

View File

@ -244,7 +244,7 @@
@media (max-width: 600px) {
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;
}
@ -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 {
position: absolute;
top: 50%;
@ -644,48 +656,6 @@
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) {
padding: 0.5rem !important;
max-height: 35rem;
@ -705,21 +675,6 @@
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 {
position: absolute;
z-index: 1;

View File

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

View File

@ -4,7 +4,7 @@ import { StoryViewerOrigin } from '../../../types';
import { ANIMATION_END_DELAY } from '../../../config';
import fastBlur from '../../../lib/fastBlur';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { getUserStoryHtmlId } from '../../../global/helpers';
import { getPeerStoryHtmlId } from '../../../global/helpers';
import { applyStyles } from '../../../util/animation';
import stopEvent from '../../../util/stopEvent';
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) {
let containerSelector;
const mediaSelector = `#${getUserStoryHtmlId(userId)}`;
const mediaSelector = `#${getPeerStoryHtmlId(userId)}`;
switch (origin) {
case StoryViewerOrigin.StoryRibbon:

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