Introduce Channel Stories (#3888)
This commit is contained in:
parent
0d843112fa
commit
8fc3df855d
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()) }),
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)]);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -12,7 +12,7 @@ const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
||||
|
||||
export type StoryRepairInfo = {
|
||||
storyData?: {
|
||||
userId: string;
|
||||
peerId: string;
|
||||
id: number;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 }),
|
||||
}), {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -194,7 +194,7 @@ export interface ApiInvoice {
|
||||
receiptMsgId?: number;
|
||||
isTest?: boolean;
|
||||
isRecurring?: boolean;
|
||||
recurringTermsUrl?: string;
|
||||
termsUrl?: string;
|
||||
extendedMedia?: ApiMessageExtendedMediaPreview;
|
||||
maxTipAmount?: number;
|
||||
suggestedTipAmounts?: number[];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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> = ({
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
27
src/components/common/Icon.tsx
Normal file
27
src/components/common/Icon.tsx
Normal 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;
|
||||
@ -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 {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -355,7 +355,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
},
|
||||
},
|
||||
stories: {
|
||||
orderedUserIds: {
|
||||
orderedPeerIds: {
|
||||
archived: archivedStories,
|
||||
},
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)}`;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -630,7 +630,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
isOpen={isReportModalOpen}
|
||||
onClose={closeReportModal}
|
||||
subject="peer"
|
||||
chatId={chat.id}
|
||||
peerId={chat.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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> = ({
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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),
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -136,6 +136,7 @@
|
||||
}
|
||||
|
||||
.tos-checkbox {
|
||||
margin-left: 0.5rem;
|
||||
padding-left: 4rem;
|
||||
|
||||
:global(.Checkbox-main) {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
@ -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 && (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
84
src/components/story/StoryFooter.module.scss
Normal file
84
src/components/story/StoryFooter.module.scss
Normal 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;
|
||||
}
|
||||
160
src/components/story/StoryFooter.tsx
Normal file
160
src/components/story/StoryFooter.tsx
Normal 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);
|
||||
@ -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));
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.user {
|
||||
.peer {
|
||||
flex: 0 0 3.75rem;
|
||||
width: 3.75rem;
|
||||
display: flex;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user