diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 128ea148a..41d04b8ae 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -16,7 +16,7 @@ import type { import { pick } from '../../../util/iteratees'; import { buildApiPhoto, buildApiThumbnailFromStripped } from './common'; -import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messages'; +import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messageContent'; import { buildStickerFromDocument } from './symbols'; import localDb from '../localDb'; import { buildApiPeerId } from './peers'; diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 00a02a11c..b0a6e9636 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -23,7 +23,7 @@ import { } from './peers'; import { omitVirtualClassFields } from './helpers'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; -import { buildApiReaction } from './messages'; +import { buildApiReaction } from './reactions'; import { buildApiUsernames } from './common'; type PeerEntityApiChatFields = Omit { + if (rule instanceof GramJs.PrivacyValueAllowAll) { + visibility ||= 'everybody'; + } else if (rule instanceof GramJs.PrivacyValueAllowContacts) { + visibility ||= 'contacts'; + } else if (rule instanceof GramJs.PrivacyValueAllowCloseFriends) { + visibility ||= 'closeFriends'; + } else if (rule instanceof GramJs.PrivacyValueDisallowContacts) { + visibility ||= 'nonContacts'; + } else if (rule instanceof GramJs.PrivacyValueDisallowAll) { + visibility ||= 'nobody'; + } else if (rule instanceof GramJs.PrivacyValueAllowUsers) { + visibility ||= 'selectedContacts'; + allowUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user')); + } else if (rule instanceof GramJs.PrivacyValueDisallowUsers) { + blockUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user')); + } else if (rule instanceof GramJs.PrivacyValueAllowChatParticipants) { + allowChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat')); + } else if (rule instanceof GramJs.PrivacyValueDisallowChatParticipants) { + blockChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat')); + } + }); + + if (!visibility) { + // Disallow by default + visibility = 'nobody'; + } + + return { + visibility, + allowUserIds: allowUserIds || [], + allowChatIds: allowChatIds || [], + blockUserIds: blockUserIds || [], + blockChatIds: blockChatIds || [], + }; +} + +export function buildApiFormattedText(textWithEntities: GramJs.TextWithEntities): ApiFormattedText { + const { text, entities } = textWithEntities; + + return { + text, + entities: entities.map(buildApiMessageEntity), + }; +} + +export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMessageEntity { + const { + className: type, offset, length, + } = entity; + + if (entity instanceof GramJs.MessageEntityMentionName) { + return { + type: ApiMessageEntityTypes.MentionName, + offset, + length, + userId: buildApiPeerId(entity.userId, 'user'), + }; + } + + if (entity instanceof GramJs.MessageEntityTextUrl) { + return { + type: ApiMessageEntityTypes.TextUrl, + offset, + length, + url: entity.url, + }; + } + + if (entity instanceof GramJs.MessageEntityPre) { + return { + type: ApiMessageEntityTypes.Pre, + offset, + length, + language: entity.language, + }; + } + + if (entity instanceof GramJs.MessageEntityCustomEmoji) { + return { + type: ApiMessageEntityTypes.CustomEmoji, + offset, + length, + documentId: entity.documentId.toString(), + }; + } + + return { + type: type as `${ApiMessageEntityDefault['type']}`, + offset, + length, + }; +} diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts new file mode 100644 index 000000000..0181bdc68 --- /dev/null +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -0,0 +1,644 @@ +import { Api as GramJs } from '../../../lib/gramjs'; + +import type { + ApiFormattedText, + ApiMessage, + ApiWebPage, + ApiWebPageStoryData, + ApiWebDocument, + ApiMessageExtendedMediaPreview, + ApiPoll, + ApiInvoice, + ApiGame, + ApiLocation, + ApiContact, + ApiDocument, + ApiVoice, + ApiAudio, + ApiVideo, + ApiPhoto, + ApiSticker, + ApiMessageStoryData, +} from '../../types'; +import type { UniversalMessage } from './messages'; + +import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_WEBM_TYPE } from '../../../config'; +import { pick } from '../../../util/iteratees'; +import { addStoryToLocalDb, serializeBytes } from '../helpers'; +import { + buildApiMessageEntity, + buildApiPhoto, + buildApiPhotoSize, + buildApiThumbnailFromPath, + buildApiThumbnailFromStripped, +} from './common'; +import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; +import { buildStickerFromDocument } from './symbols'; + +export function buildMessageContent( + mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification, +) { + let content: ApiMessage['content'] = {}; + + if (mtpMessage.media) { + content = { + ...buildMessageMediaContent(mtpMessage.media), + }; + } + + const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported; + + if (mtpMessage.message && !hasUnsupportedMedia + && !content.sticker && !content.poll && !content.contact && !(content.video?.isRound)) { + content = { + ...content, + text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities), + }; + } + + return content; +} + +export function buildMessageTextContent( + message: string, + entities?: GramJs.TypeMessageEntity[], +): ApiFormattedText { + return { + text: message, + ...(entities && { entities: entities.map(buildApiMessageEntity) }), + }; +} + +export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMessage['content'] | undefined { + if ('ttlSeconds' in media && media.ttlSeconds) { + return undefined; + } + + if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) { + return buildMessageMediaContent(media.extendedMedia.media); + } + + const sticker = buildSticker(media); + if (sticker) return { sticker }; + + const photo = buildPhoto(media); + if (photo) return { photo }; + + const video = buildVideo(media); + const altVideo = buildAltVideo(media); + if (video) return { video, altVideo }; + + const audio = buildAudio(media); + if (audio) return { audio }; + + const voice = buildVoice(media); + if (voice) return { voice }; + + const document = buildDocumentFromMedia(media); + if (document) return { document }; + + const contact = buildContact(media); + if (contact) return { contact }; + + const poll = buildPollFromMedia(media); + if (poll) return { poll }; + + const webPage = buildWebPage(media); + if (webPage) return { webPage }; + + const invoice = buildInvoiceFromMedia(media); + if (invoice) return { invoice }; + + const location = buildLocationFromMedia(media); + if (location) return { location }; + + const game = buildGameFromMedia(media); + if (game) return { game }; + + const storyData = buildMessageStoryData(media); + if (storyData) return { storyData }; + + return undefined; +} + +function buildSticker(media: GramJs.TypeMessageMedia): ApiSticker | undefined { + if ( + !(media instanceof GramJs.MessageMediaDocument) + || !media.document + || !(media.document instanceof GramJs.Document) + ) { + return undefined; + } + + return buildStickerFromDocument(media.document, media.nopremium); +} + +function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined { + if (!(media instanceof GramJs.MessageMediaPhoto) || !media.photo || !(media.photo instanceof GramJs.Photo)) { + return undefined; + } + + return buildApiPhoto(media.photo, media.spoiler); +} + +export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: boolean): ApiVideo | undefined { + if (document instanceof GramJs.DocumentEmpty) { + return undefined; + } + + const { + id, mimeType, thumbs, size, attributes, + } = document; + + // eslint-disable-next-line no-restricted-globals + if (mimeType === VIDEO_WEBM_TYPE && !(self as any).isWebmSupported) { + return undefined; + } + + const videoAttr = attributes + .find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo); + + if (!videoAttr) { + return undefined; + } + + const gifAttr = attributes + .find((a: any): a is GramJs.DocumentAttributeAnimated => a instanceof GramJs.DocumentAttributeAnimated); + + const { + duration, + w: width, + h: height, + supportsStreaming = false, + roundMessage: isRound = false, + nosound, + } = videoAttr; + + return { + id: String(id), + mimeType, + duration, + fileName: getFilenameFromDocument(document, 'video'), + width, + height, + supportsStreaming, + isRound, + isGif: Boolean(gifAttr), + thumbnail: buildApiThumbnailFromStripped(thumbs), + size: size.toJSNumber(), + isSpoiler, + ...(nosound && { noSound: true }), + }; +} + +function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined { + if ( + !(media instanceof GramJs.MessageMediaDocument) + || !(media.document instanceof GramJs.Document) + || !media.document.mimeType.startsWith('video') + ) { + return undefined; + } + + return buildVideoFromDocument(media.document, media.spoiler); +} + +function buildAltVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined { + if ( + !(media instanceof GramJs.MessageMediaDocument) + || !(media.altDocument instanceof GramJs.Document) + || !media.altDocument.mimeType.startsWith('video') + ) { + return undefined; + } + + return buildVideoFromDocument(media.altDocument, media.spoiler); +} + +function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined { + if ( + !(media instanceof GramJs.MessageMediaDocument) + || !media.document + || !(media.document instanceof GramJs.Document) + ) { + return undefined; + } + + const audioAttribute = media.document.attributes + .find((attr: any): attr is GramJs.DocumentAttributeAudio => ( + attr instanceof GramJs.DocumentAttributeAudio + )); + + if (!audioAttribute || audioAttribute.voice) { + return undefined; + } + + const thumbnailSizes = media.document.thumbs && media.document.thumbs + .filter((thumb): thumb is GramJs.PhotoSize => thumb instanceof GramJs.PhotoSize) + .map((thumb) => buildApiPhotoSize(thumb)); + + return { + id: String(media.document.id), + fileName: getFilenameFromDocument(media.document, 'audio'), + thumbnailSizes, + size: media.document.size.toJSNumber(), + ...pick(media.document, ['mimeType']), + ...pick(audioAttribute, ['duration', 'performer', 'title']), + }; +} + +function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined { + if ( + !(media instanceof GramJs.MessageMediaDocument) + || !media.document + || !(media.document instanceof GramJs.Document) + ) { + return undefined; + } + + const audioAttribute = media.document.attributes + .find((attr: any): attr is GramJs.DocumentAttributeAudio => ( + attr instanceof GramJs.DocumentAttributeAudio + )); + + if (!audioAttribute || !audioAttribute.voice) { + return undefined; + } + + const { duration, waveform } = audioAttribute; + + return { + id: String(media.document.id), + duration, + waveform: waveform ? Array.from(waveform) : undefined, + }; +} + +function buildDocumentFromMedia(media: GramJs.TypeMessageMedia) { + if (!(media instanceof GramJs.MessageMediaDocument) || !media.document) { + return undefined; + } + + return buildApiDocument(media.document); +} + +export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | undefined { + if (!(document instanceof GramJs.Document)) { + return undefined; + } + + const { + id, size, mimeType, date, thumbs, attributes, + } = document; + + const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize); + let thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs); + if (!thumbnail && thumbs && photoSize) { + const photoPath = thumbs.find((s: any): s is GramJs.PhotoPathSize => s instanceof GramJs.PhotoPathSize); + if (photoPath) { + thumbnail = buildApiThumbnailFromPath(photoPath, photoSize); + } + } + + let mediaType: ApiDocument['mediaType'] | undefined; + let mediaSize: ApiDocument['mediaSize'] | undefined; + if (photoSize) { + mediaSize = { + width: photoSize.w, + height: photoSize.h, + }; + + if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { + mediaType = 'photo'; + + const imageAttribute = attributes + .find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize); + + if (imageAttribute) { + const { w: width, h: height } = imageAttribute; + mediaSize = { + width, + height, + }; + } + } else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { + mediaType = 'video'; + const videoAttribute = attributes + .find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo); + + if (videoAttribute) { + const { w: width, h: height } = videoAttribute; + mediaSize = { + width, + height, + }; + } + } + } + + return { + id: String(id), + size: size.toJSNumber(), + mimeType, + timestamp: date, + fileName: getFilenameFromDocument(document), + thumbnail, + mediaType, + mediaSize, + }; +} + +function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined { + if (!(media instanceof GramJs.MessageMediaContact)) { + return undefined; + } + + const { + firstName, lastName, phoneNumber, userId, + } = media; + + return { + firstName, lastName, phoneNumber, userId: buildApiPeerId(userId, 'user'), + }; +} + +function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined { + if (!(media instanceof GramJs.MessageMediaPoll)) { + return undefined; + } + + return buildPoll(media.poll, media.results); +} + +function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiInvoice | undefined { + if (!(media instanceof GramJs.MessageMediaInvoice)) { + return undefined; + } + + return buildInvoice(media); +} + +function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | undefined { + if (media instanceof GramJs.MessageMediaGeo) { + return buildGeo(media); + } + + if (media instanceof GramJs.MessageMediaVenue) { + return buildVenue(media); + } + + if (media instanceof GramJs.MessageMediaGeoLive) { + return buildGeoLive(media); + } + + return undefined; +} + +function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined { + const point = buildGeoPoint(media.geo); + return point && { type: 'geo', geo: point }; +} + +function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined { + const { + geo, title, provider, address, venueId, venueType, + } = media; + const point = buildGeoPoint(geo); + return point && { + type: 'venue', + geo: point, + title, + provider, + address, + venueId, + venueType, + }; +} + +function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefined { + const { geo, period, heading } = media; + const point = buildGeoPoint(geo); + return point && { + type: 'geoLive', + geo: point, + period, + heading, + }; +} + +export function buildGeoPoint(geo: GramJs.TypeGeoPoint): ApiLocation['geo'] | undefined { + if (geo instanceof GramJs.GeoPointEmpty) return undefined; + const { + long, lat, accuracyRadius, accessHash, + } = geo; + return { + long, + lat, + accessHash: accessHash.toString(), + accuracyRadius, + }; +} + +function buildGameFromMedia(media: GramJs.TypeMessageMedia): ApiGame | undefined { + if (!(media instanceof GramJs.MessageMediaGame)) { + return undefined; + } + + return buildGame(media); +} + +function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined { + const { + id, accessHash, shortName, title, description, photo: apiPhoto, document: apiDocument, + } = media.game; + + const photo = apiPhoto instanceof GramJs.Photo ? buildApiPhoto(apiPhoto) : undefined; + const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined; + + return { + id: id.toString(), + accessHash: accessHash.toString(), + shortName, + title, + description, + photo, + document, + }; +} + +export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessageStoryData | undefined { + if (!(media instanceof GramJs.MessageMediaStory)) { + return undefined; + } + + const userId = buildApiPeerId(media.userId, 'user'); + + return { id: media.id, userId, ...(media.viaMention && { isMention: true }) }; +} + +export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll { + const { id, answers: rawAnswers } = poll; + const answers = rawAnswers.map((answer) => ({ + text: answer.text, + option: serializeBytes(answer.option), + })); + + return { + id: String(id), + summary: { + isPublic: poll.publicVoters, + ...pick(poll, [ + 'closed', + 'multipleChoice', + 'quiz', + 'question', + 'closePeriod', + 'closeDate', + ]), + answers, + }, + results: buildPollResults(pollResults), + }; +} + +export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { + const { + description: text, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia, + } = media; + + const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview + ? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined; + + return { + title, + text, + photo: buildApiWebDocument(photo), + receiptMsgId, + amount: Number(totalAmount), + currency, + isTest: test, + extendedMedia: preview, + }; +} + +export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] { + const { + results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min, + } = pollResults; + const results = rawResults?.map(({ + option, chosen, correct, voters, + }) => ({ + isChosen: chosen, + isCorrect: correct, + option: serializeBytes(option), + votersCount: voters, + })); + + return { + isMin: min, + totalVoters, + recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)), + results, + solution, + ...(entities && { solutionEntities: entities.map(buildApiMessageEntity) }), + }; +} + +export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined { + if ( + !(media instanceof GramJs.MessageMediaWebPage) + || !(media.webpage instanceof GramJs.WebPage) + ) { + return undefined; + } + + const { + id, photo, document, attributes, + } = media.webpage; + + let video; + if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) { + video = buildVideoFromDocument(document); + } + let story: ApiWebPageStoryData | undefined; + const attributeStory = attributes + ?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); + if (attributeStory) { + const userId = buildApiPeerId(attributeStory.userId, 'user'); + story = { + id: attributeStory.id, + userId, + }; + + if (attributeStory.story instanceof GramJs.StoryItem) { + addStoryToLocalDb(attributeStory.story, userId); + } + } + + return { + id: Number(id), + ...pick(media.webpage, [ + 'url', + 'displayUrl', + 'type', + 'siteName', + 'title', + 'description', + 'duration', + ]), + photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined, + document: !video && document ? buildApiDocument(document) : undefined, + video, + story, + }; +} + +function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') { + const { mimeType, attributes } = document; + const filenameAttribute = attributes + .find((a: any): a is GramJs.DocumentAttributeFilename => a instanceof GramJs.DocumentAttributeFilename); + + if (filenameAttribute) { + return filenameAttribute.fileName; + } + + const extension = mimeType.split('/')[1]; + + return `${defaultBase}${String(document.id)}.${extension}`; +} + +export function buildApiMessageExtendedMediaPreview( + preview: GramJs.MessageExtendedMediaPreview, +): ApiMessageExtendedMediaPreview { + const { + w, h, thumb, videoDuration, + } = preview; + + return { + width: w, + height: h, + duration: videoDuration, + thumbnail: thumb ? buildApiThumbnailFromStripped([thumb]) : undefined, + }; +} + +export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined { + if (!document) return undefined; + + const { + url, size, mimeType, + } = document; + const accessHash = document instanceof GramJs.WebDocument ? document.accessHash.toString() : undefined; + const sizeAttr = document.attributes.find((attr): attr is GramJs.DocumentAttributeImageSize => ( + attr instanceof GramJs.DocumentAttributeImageSize + )); + const dimensions = sizeAttr && { width: sizeAttr.w, height: sizeAttr.h }; + + return { + url, + accessHash, + size, + mimeType, + dimensions, + }; +} diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index b8beca76a..cf6c4c997 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -5,48 +5,26 @@ import type { ApiPhoto, ApiSticker, ApiVideo, - ApiVoice, - ApiAudio, - ApiDocument, ApiAction, ApiContact, ApiAttachment, - ApiPoll, ApiNewPoll, - ApiWebPage, ApiMessageEntity, - ApiFormattedText, ApiReplyKeyboard, ApiKeyboardButton, ApiChat, ApiThreadInfo, - ApiInvoice, ApiGroupCall, - ApiReactions, - ApiReactionCount, - ApiPeerReaction, - ApiAvailableReaction, ApiSponsoredMessage, ApiUser, - ApiLocation, - ApiGame, PhoneCallAction, - ApiWebDocument, - ApiMessageEntityDefault, - ApiMessageExtendedMediaPreview, - ApiReaction, - ApiReactionEmoji, ApiTypeReplyTo, ApiStory, ApiStorySkipped, - ApiWebPageStoryData, - ApiMessageStoryData, - ApiTypeStory, } from '../../types'; import { ApiMessageEntityTypes, } from '../../types'; -import type { ApiPrivacySettings, PrivacyVisibility } from '../../../types'; import { DELETED_COMMENTS_CHANNEL_ID, @@ -55,25 +33,24 @@ import { SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, - VIDEO_WEBM_TYPE, } from '../../../config'; -import { buildCollectionByCallback, pick } from '../../../util/iteratees'; -import { buildStickerFromDocument } from './symbols'; +import { pick } from '../../../util/iteratees'; +import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage'; +import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; +import { buildMessageContent, buildMessageTextContent } from './messageContent'; import { - buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromPath, buildApiThumbnailFromStripped, + buildApiPhoto, } from './common'; import { interpolateArray } from '../../../util/waveform'; import { buildPeer } from '../gramjsBuilders'; import { addPhotoToLocalDb, - addStoryToLocalDb, resolveMessageApiChatId, serializeBytes, } from '../helpers'; import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers'; +import { buildMessageReactions } from './reactions'; import { buildApiCallDiscardReason } from './calls'; -import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage'; -import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; const LOCAL_MESSAGES_LIMIT = 1e6; // 1M @@ -165,7 +142,7 @@ export function buildApiMessageFromNotification( }; } -type UniversalMessage = ( +export type UniversalMessage = ( Pick & Pick, ( 'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' | @@ -268,132 +245,6 @@ export function buildApiMessageWithChatId( }; } -export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions { - const { - recentReactions, results, canSeeList, - } = reactions; - - return { - canSeeList, - results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator), - recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean), - }; -} - -function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) { - const diff = b.count - a.count; - if (diff) return diff; - if (a.chosenOrder !== undefined && b.chosenOrder !== undefined) { - return a.chosenOrder - b.chosenOrder; - } - if (a.chosenOrder !== undefined) return 1; - if (b.chosenOrder !== undefined) return -1; - return 0; -} - -function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined { - const { chosenOrder, count, reaction } = reactionCount; - - const apiReaction = buildApiReaction(reaction); - if (!apiReaction) return undefined; - - return { - chosenOrder, - count, - reaction: apiReaction, - }; -} - -export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiPeerReaction | undefined { - const { - peerId, reaction, big, unread, date, my, - } = userReaction; - - const apiReaction = buildApiReaction(reaction); - if (!apiReaction) return undefined; - - return { - peerId: getApiChatIdFromMtpPeer(peerId), - reaction: apiReaction, - addedDate: date, - isUnread: unread, - isBig: big, - isOwn: my, - }; -} - -export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | undefined { - if (reaction instanceof GramJs.ReactionEmoji) { - return { - emoticon: reaction.emoticon, - }; - } - - if (reaction instanceof GramJs.ReactionCustomEmoji) { - return { - documentId: reaction.documentId.toString(), - }; - } - - return undefined; -} - -export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction { - const { - selectAnimation, staticIcon, reaction, title, appearAnimation, - inactive, aroundAnimation, centerIcon, effectAnimation, activateAnimation, - premium, - } = availableReaction; - - return { - selectAnimation: buildApiDocument(selectAnimation), - appearAnimation: buildApiDocument(appearAnimation), - activateAnimation: buildApiDocument(activateAnimation), - effectAnimation: buildApiDocument(effectAnimation), - staticIcon: buildApiDocument(staticIcon), - aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined, - centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined, - reaction: { emoticon: reaction } as ApiReactionEmoji, - title, - isInactive: inactive, - isPremium: premium, - }; -} - -export function buildMessageContent( - mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification, -) { - let content: ApiMessage['content'] = {}; - - if (mtpMessage.media) { - content = { - ...buildMessageMediaContent(mtpMessage.media), - }; - } - - const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported; - - if (mtpMessage.message && !hasUnsupportedMedia - && !content.sticker && !content.poll && !content.contact && !(content.video?.isRound)) { - content = { - ...content, - text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities), - }; - } - - return content; -} - -export function buildMessageTextContent( - message: string, - entities?: GramJs.TypeMessageEntity[], -): ApiFormattedText { - return { - text: message, - ...(entities && { entities: entities.map(buildApiMessageEntity) }), - }; -} - export function buildMessageDraft(draft: GramJs.TypeDraftMessage) { if (draft instanceof GramJs.DraftMessageEmpty) { return undefined; @@ -410,58 +261,6 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage) { }; } -export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMessage['content'] | undefined { - if ('ttlSeconds' in media && media.ttlSeconds) { - return undefined; - } - - if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) { - return buildMessageMediaContent(media.extendedMedia.media); - } - - const sticker = buildSticker(media); - if (sticker) return { sticker }; - - const photo = buildPhoto(media); - if (photo) return { photo }; - - const video = buildVideo(media); - const altVideo = buildAltVideo(media); - if (video) return { video, altVideo }; - - const audio = buildAudio(media); - if (audio) return { audio }; - - const voice = buildVoice(media); - if (voice) return { voice }; - - const document = buildDocumentFromMedia(media); - if (document) return { document }; - - const contact = buildContact(media); - if (contact) return { contact }; - - const poll = buildPollFromMedia(media); - if (poll) return { poll }; - - const webPage = buildWebPage(media); - if (webPage) return { webPage }; - - const invoice = buildInvoiceFromMedia(media); - if (invoice) return { invoice }; - - const location = buildLocationFromMedia(media); - if (location) return { location }; - - const game = buildGameFromMedia(media); - if (game) return { game }; - - const storyData = buildMessageStoryData(media); - if (storyData) return { storyData }; - - return undefined; -} - function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWithSelf = false): ApiMessageForwardInfo { const savedFromPeerId = fwdFrom.savedFromPeer && getApiChatIdFromMtpPeer(fwdFrom.savedFromPeer); const fromId = fwdFrom.fromId && getApiChatIdFromMtpPeer(fwdFrom.fromId); @@ -480,478 +279,6 @@ function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWith }; } -function buildSticker(media: GramJs.TypeMessageMedia): ApiSticker | undefined { - if ( - !(media instanceof GramJs.MessageMediaDocument) - || !media.document - || !(media.document instanceof GramJs.Document) - ) { - return undefined; - } - - return buildStickerFromDocument(media.document, media.nopremium); -} - -function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined { - if (!(media instanceof GramJs.MessageMediaPhoto) || !media.photo || !(media.photo instanceof GramJs.Photo)) { - return undefined; - } - - return buildApiPhoto(media.photo, media.spoiler); -} - -export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: boolean): ApiVideo | undefined { - if (document instanceof GramJs.DocumentEmpty) { - return undefined; - } - - const { - id, mimeType, thumbs, size, attributes, - } = document; - - // eslint-disable-next-line no-restricted-globals - if (mimeType === VIDEO_WEBM_TYPE && !(self as any).isWebmSupported) { - return undefined; - } - - const videoAttr = attributes - .find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo); - - if (!videoAttr) { - return undefined; - } - - const gifAttr = attributes - .find((a: any): a is GramJs.DocumentAttributeAnimated => a instanceof GramJs.DocumentAttributeAnimated); - - const { - duration, - w: width, - h: height, - supportsStreaming = false, - roundMessage: isRound = false, - nosound, - } = videoAttr; - - return { - id: String(id), - mimeType, - duration, - fileName: getFilenameFromDocument(document, 'video'), - width, - height, - supportsStreaming, - isRound, - isGif: Boolean(gifAttr), - thumbnail: buildApiThumbnailFromStripped(thumbs), - size: size.toJSNumber(), - isSpoiler, - ...(nosound && { noSound: true }), - }; -} - -function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined { - if ( - !(media instanceof GramJs.MessageMediaDocument) - || !(media.document instanceof GramJs.Document) - || !media.document.mimeType.startsWith('video') - ) { - return undefined; - } - - return buildVideoFromDocument(media.document, media.spoiler); -} - -function buildAltVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined { - if ( - !(media instanceof GramJs.MessageMediaDocument) - || !(media.altDocument instanceof GramJs.Document) - || !media.altDocument.mimeType.startsWith('video') - ) { - return undefined; - } - - return buildVideoFromDocument(media.altDocument, media.spoiler); -} - -function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined { - if ( - !(media instanceof GramJs.MessageMediaDocument) - || !media.document - || !(media.document instanceof GramJs.Document) - ) { - return undefined; - } - - const audioAttribute = media.document.attributes - .find((attr: any): attr is GramJs.DocumentAttributeAudio => ( - attr instanceof GramJs.DocumentAttributeAudio - )); - - if (!audioAttribute || audioAttribute.voice) { - return undefined; - } - - const thumbnailSizes = media.document.thumbs && media.document.thumbs - .filter((thumb): thumb is GramJs.PhotoSize => thumb instanceof GramJs.PhotoSize) - .map((thumb) => buildApiPhotoSize(thumb)); - - return { - id: String(media.document.id), - fileName: getFilenameFromDocument(media.document, 'audio'), - thumbnailSizes, - size: media.document.size.toJSNumber(), - ...pick(media.document, ['mimeType']), - ...pick(audioAttribute, ['duration', 'performer', 'title']), - }; -} - -function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined { - if ( - !(media instanceof GramJs.MessageMediaDocument) - || !media.document - || !(media.document instanceof GramJs.Document) - ) { - return undefined; - } - - const audioAttribute = media.document.attributes - .find((attr: any): attr is GramJs.DocumentAttributeAudio => ( - attr instanceof GramJs.DocumentAttributeAudio - )); - - if (!audioAttribute || !audioAttribute.voice) { - return undefined; - } - - const { duration, waveform } = audioAttribute; - - return { - id: String(media.document.id), - duration, - waveform: waveform ? Array.from(waveform) : undefined, - }; -} - -function buildDocumentFromMedia(media: GramJs.TypeMessageMedia) { - if (!(media instanceof GramJs.MessageMediaDocument) || !media.document) { - return undefined; - } - - return buildApiDocument(media.document); -} - -export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | undefined { - if (!(document instanceof GramJs.Document)) { - return undefined; - } - - const { - id, size, mimeType, date, thumbs, attributes, - } = document; - - const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize); - let thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs); - if (!thumbnail && thumbs && photoSize) { - const photoPath = thumbs.find((s: any): s is GramJs.PhotoPathSize => s instanceof GramJs.PhotoPathSize); - if (photoPath) { - thumbnail = buildApiThumbnailFromPath(photoPath, photoSize); - } - } - - let mediaType: ApiDocument['mediaType'] | undefined; - let mediaSize: ApiDocument['mediaSize'] | undefined; - if (photoSize) { - mediaSize = { - width: photoSize.w, - height: photoSize.h, - }; - - if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { - mediaType = 'photo'; - - const imageAttribute = attributes - .find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize); - - if (imageAttribute) { - const { w: width, h: height } = imageAttribute; - mediaSize = { - width, - height, - }; - } - } else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { - mediaType = 'video'; - const videoAttribute = attributes - .find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo); - - if (videoAttribute) { - const { w: width, h: height } = videoAttribute; - mediaSize = { - width, - height, - }; - } - } - } - - return { - id: String(id), - size: size.toJSNumber(), - mimeType, - timestamp: date, - fileName: getFilenameFromDocument(document), - thumbnail, - mediaType, - mediaSize, - }; -} - -function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined { - if (!(media instanceof GramJs.MessageMediaContact)) { - return undefined; - } - - const { - firstName, lastName, phoneNumber, userId, - } = media; - - return { - firstName, lastName, phoneNumber, userId: buildApiPeerId(userId, 'user'), - }; -} - -function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined { - if (!(media instanceof GramJs.MessageMediaPoll)) { - return undefined; - } - - return buildPoll(media.poll, media.results); -} - -function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiInvoice | undefined { - if (!(media instanceof GramJs.MessageMediaInvoice)) { - return undefined; - } - - return buildInvoice(media); -} - -function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | undefined { - if (media instanceof GramJs.MessageMediaGeo) { - return buildGeo(media); - } - - if (media instanceof GramJs.MessageMediaVenue) { - return buildVenue(media); - } - - if (media instanceof GramJs.MessageMediaGeoLive) { - return buildGeoLive(media); - } - - return undefined; -} - -function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined { - const point = buildGeoPoint(media.geo); - return point && { type: 'geo', geo: point }; -} - -function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined { - const { - geo, title, provider, address, venueId, venueType, - } = media; - const point = buildGeoPoint(geo); - return point && { - type: 'venue', - geo: point, - title, - provider, - address, - venueId, - venueType, - }; -} - -function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefined { - const { geo, period, heading } = media; - const point = buildGeoPoint(geo); - return point && { - type: 'geoLive', - geo: point, - period, - heading, - }; -} - -function buildGeoPoint(geo: GramJs.TypeGeoPoint): ApiLocation['geo'] | undefined { - if (geo instanceof GramJs.GeoPointEmpty) return undefined; - const { - long, lat, accuracyRadius, accessHash, - } = geo; - return { - long, - lat, - accessHash: accessHash.toString(), - accuracyRadius, - }; -} - -function buildGameFromMedia(media: GramJs.TypeMessageMedia): ApiGame | undefined { - if (!(media instanceof GramJs.MessageMediaGame)) { - return undefined; - } - - return buildGame(media); -} - -function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined { - const { - id, accessHash, shortName, title, description, photo: apiPhoto, document: apiDocument, - } = media.game; - - const photo = apiPhoto instanceof GramJs.Photo ? buildApiPhoto(apiPhoto) : undefined; - const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined; - - return { - id: id.toString(), - accessHash: accessHash.toString(), - shortName, - title, - description, - photo, - document, - }; -} - -export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll { - const { id, answers: rawAnswers } = poll; - const answers = rawAnswers.map((answer) => ({ - text: answer.text, - option: serializeBytes(answer.option), - })); - - return { - id: String(id), - summary: { - isPublic: poll.publicVoters, - ...pick(poll, [ - 'closed', - 'multipleChoice', - 'quiz', - 'question', - 'closePeriod', - 'closeDate', - ]), - answers, - }, - results: buildPollResults(pollResults), - }; -} - -export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { - const { - description: text, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia, - } = media; - - const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview - ? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined; - - return { - title, - text, - photo: buildApiWebDocument(photo), - receiptMsgId, - amount: Number(totalAmount), - currency, - isTest: test, - extendedMedia: preview, - }; -} - -export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] { - const { - results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min, - } = pollResults; - const results = rawResults?.map(({ - option, chosen, correct, voters, - }) => ({ - isChosen: chosen, - isCorrect: correct, - option: serializeBytes(option), - votersCount: voters, - })); - - return { - isMin: min, - totalVoters, - recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)), - results, - solution, - ...(entities && { solutionEntities: entities.map(buildApiMessageEntity) }), - }; -} - -export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined { - if ( - !(media instanceof GramJs.MessageMediaWebPage) - || !(media.webpage instanceof GramJs.WebPage) - ) { - return undefined; - } - - const { - id, photo, document, attributes, - } = media.webpage; - - let video; - if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) { - video = buildVideoFromDocument(document); - } - let story: ApiWebPageStoryData | undefined; - const attributeStory = attributes - ?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); - if (attributeStory) { - const userId = buildApiPeerId(attributeStory.userId, 'user'); - story = { - id: attributeStory.id, - userId, - }; - - if (attributeStory.story instanceof GramJs.StoryItem) { - addStoryToLocalDb(attributeStory.story, userId); - } - } - - return { - id: Number(id), - ...pick(media.webpage, [ - 'url', - 'displayUrl', - 'type', - 'siteName', - 'title', - 'description', - 'duration', - ]), - photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined, - document: !video && document ? buildApiDocument(document) : undefined, - video, - story, - }; -} - -function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessageStoryData | undefined { - if (!(media instanceof GramJs.MessageMediaStory)) { - return undefined; - } - - const userId = buildApiPeerId(media.userId, 'user'); - - return { id: media.id, userId, ...(media.viaMention && { isMention: true }) }; -} - function buildAction( action: GramJs.TypeMessageAction, senderId: string | undefined, @@ -1338,18 +665,14 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool }; } -function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') { - const { mimeType, attributes } = document; - const filenameAttribute = attributes - .find((a: any): a is GramJs.DocumentAttributeFilename => a instanceof GramJs.DocumentAttributeFilename); - - if (filenameAttribute) { - return filenameAttribute.fileName; - } - - const extension = mimeType.split('/')[1]; - - return `${defaultBase}${String(document.id)}.${extension}`; +function buildNewPoll(poll: ApiNewPoll, localId: number) { + return { + poll: { + id: String(localId), + summary: pick(poll.summary, ['question', 'answers']), + results: {}, + }, + }; } export function buildLocalMessage( @@ -1583,100 +906,6 @@ function buildUploadingMedia( }; } -export function buildApiMessageExtendedMediaPreview( - preview: GramJs.MessageExtendedMediaPreview, -): ApiMessageExtendedMediaPreview { - const { - w, h, thumb, videoDuration, - } = preview; - - return { - width: w, - height: h, - duration: videoDuration, - thumbnail: thumb ? buildApiThumbnailFromStripped([thumb]) : undefined, - }; -} - -export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined { - if (!document) return undefined; - - const { - url, size, mimeType, - } = document; - const accessHash = document instanceof GramJs.WebDocument ? document.accessHash.toString() : undefined; - const sizeAttr = document.attributes.find((attr): attr is GramJs.DocumentAttributeImageSize => ( - attr instanceof GramJs.DocumentAttributeImageSize - )); - const dimensions = sizeAttr && { width: sizeAttr.w, height: sizeAttr.h }; - - return { - url, - accessHash, - size, - mimeType, - dimensions, - }; -} - -function buildNewPoll(poll: ApiNewPoll, localId: number) { - return { - poll: { - id: String(localId), - summary: pick(poll.summary, ['question', 'answers']), - results: {}, - }, - }; -} - -export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMessageEntity { - const { - className: type, offset, length, - } = entity; - - if (entity instanceof GramJs.MessageEntityMentionName) { - return { - type: ApiMessageEntityTypes.MentionName, - offset, - length, - userId: buildApiPeerId(entity.userId, 'user'), - }; - } - - if (entity instanceof GramJs.MessageEntityTextUrl) { - return { - type: ApiMessageEntityTypes.TextUrl, - offset, - length, - url: entity.url, - }; - } - - if (entity instanceof GramJs.MessageEntityPre) { - return { - type: ApiMessageEntityTypes.Pre, - offset, - length, - language: entity.language, - }; - } - - if (entity instanceof GramJs.MessageEntityCustomEmoji) { - return { - type: ApiMessageEntityTypes.CustomEmoji, - offset, - length, - documentId: entity.documentId.toString(), - }; - } - - return { - type: type as `${ApiMessageEntityDefault['type']}`, - offset, - length, - }; -} - function buildThreadInfo( messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string, ): ApiThreadInfo | undefined { @@ -1706,120 +935,3 @@ function buildThreadInfo( ...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }), }; } - -export function buildApiFormattedText(textWithEntities: GramJs.TextWithEntities): ApiFormattedText { - const { text, entities } = textWithEntities; - - return { - text, - entities: entities.map(buildApiMessageEntity), - }; -} - -export function buildApiUsersStories(userStories: GramJs.UserStories) { - const userId = buildApiPeerId(userStories.userId, 'user'); - - return buildCollectionByCallback(userStories.stories, (story) => [story.id, buildApiStory(userId, story)]); -} - -export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiTypeStory { - if (story instanceof GramJs.StoryItemDeleted) { - return { - id: story.id, - userId, - isDeleted: true, - }; - } - - if (story instanceof GramJs.StoryItemSkipped) { - const { - id, date, expireDate, closeFriends, - } = story; - - return { - id, - userId, - ...(closeFriends && { isForCloseFriends: true }), - date, - expireDate, - }; - } - - const { - edited, pinned, expireDate, id, date, caption, - entities, media, privacy, views, - public: isPublic, noforwards, closeFriends, contacts, selectedContacts, - } = story; - - const content: ApiMessage['content'] = { - ...buildMessageMediaContent(media), - }; - - if (caption) { - content.text = buildMessageTextContent(caption, entities); - } - - return { - id, - userId, - date, - expireDate, - content, - ...(isPublic && { isPublic }), - ...(edited && { isEdited: true }), - ...(pinned && { isPinned: true }), - ...(contacts && { isForContacts: true }), - ...(selectedContacts && { isForSelectedContacts: true }), - ...(closeFriends && { isForCloseFriends: true }), - ...(noforwards && { noForwards: true }), - ...(views?.viewsCount && { viewsCount: views.viewsCount }), - ...(views?.recentViewers && { - recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')), - }), - ...(privacy && { visibility: buildPrivacyRules(privacy) }), - }; -} - -export function buildPrivacyRules(rules: GramJs.TypePrivacyRule[]): ApiPrivacySettings { - let visibility: PrivacyVisibility | undefined; - let allowUserIds: string[] | undefined; - let allowChatIds: string[] | undefined; - let blockUserIds: string[] | undefined; - let blockChatIds: string[] | undefined; - - rules.forEach((rule) => { - if (rule instanceof GramJs.PrivacyValueAllowAll) { - visibility ||= 'everybody'; - } else if (rule instanceof GramJs.PrivacyValueAllowContacts) { - visibility ||= 'contacts'; - } else if (rule instanceof GramJs.PrivacyValueAllowCloseFriends) { - visibility ||= 'closeFriends'; - } else if (rule instanceof GramJs.PrivacyValueDisallowContacts) { - visibility ||= 'nonContacts'; - } else if (rule instanceof GramJs.PrivacyValueDisallowAll) { - visibility ||= 'nobody'; - } else if (rule instanceof GramJs.PrivacyValueAllowUsers) { - visibility ||= 'selectedContacts'; - allowUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user')); - } else if (rule instanceof GramJs.PrivacyValueDisallowUsers) { - blockUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user')); - } else if (rule instanceof GramJs.PrivacyValueAllowChatParticipants) { - allowChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat')); - } else if (rule instanceof GramJs.PrivacyValueDisallowChatParticipants) { - blockChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat')); - } - }); - - if (!visibility) { - // Disallow by default - visibility = 'nobody'; - } - - return { - visibility, - allowUserIds: allowUserIds || [], - allowChatIds: allowChatIds || [], - blockUserIds: blockUserIds || [], - blockChatIds: blockChatIds || [], - }; -} diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 6cc0665c4..d4eb5dfdb 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -5,7 +5,8 @@ import type { } from '../../types'; import type { ApiPrivacyKey } from '../../../types'; -import { buildApiDocument, buildApiReaction } from './messages'; +import { buildApiReaction } from './reactions'; +import { buildApiDocument } from './messageContent'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { omit, pick } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 71fca6dc7..c8091a402 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -5,7 +5,8 @@ import type { ApiPaymentForm, ApiReceipt, ApiLabeledPrice, ApiPaymentCredentials, } from '../../types'; -import { buildApiDocument, buildApiMessageEntity, buildApiWebDocument } from './messages'; +import { buildApiDocument, buildApiWebDocument } from './messageContent'; +import { buildApiMessageEntity } from './common'; import { omitVirtualClassFields } from './helpers'; export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) { diff --git a/src/api/gramjs/apiBuilders/reactions.ts b/src/api/gramjs/apiBuilders/reactions.ts new file mode 100644 index 000000000..28fc32596 --- /dev/null +++ b/src/api/gramjs/apiBuilders/reactions.ts @@ -0,0 +1,103 @@ +import { Api as GramJs } from '../../../lib/gramjs'; +import type { + ApiAvailableReaction, + ApiPeerReaction, + ApiReaction, + ApiReactionCount, + ApiReactionEmoji, + ApiReactions, +} from '../../types'; +import { buildApiDocument } from './messageContent'; +import { getApiChatIdFromMtpPeer } from './peers'; + +export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions { + const { + recentReactions, results, canSeeList, + } = reactions; + + return { + canSeeList, + results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator), + recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean), + }; +} + +function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) { + const diff = b.count - a.count; + if (diff) return diff; + if (a.chosenOrder !== undefined && b.chosenOrder !== undefined) { + return a.chosenOrder - b.chosenOrder; + } + if (a.chosenOrder !== undefined) return 1; + if (b.chosenOrder !== undefined) return -1; + return 0; +} + +function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined { + const { chosenOrder, count, reaction } = reactionCount; + + const apiReaction = buildApiReaction(reaction); + if (!apiReaction) return undefined; + + return { + chosenOrder, + count, + reaction: apiReaction, + }; +} + +export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiPeerReaction | undefined { + const { + peerId, reaction, big, unread, date, my, + } = userReaction; + + const apiReaction = buildApiReaction(reaction); + if (!apiReaction) return undefined; + + return { + peerId: getApiChatIdFromMtpPeer(peerId), + reaction: apiReaction, + addedDate: date, + isUnread: unread, + isBig: big, + isOwn: my, + }; +} + +export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | undefined { + if (reaction instanceof GramJs.ReactionEmoji) { + return { + emoticon: reaction.emoticon, + }; + } + + if (reaction instanceof GramJs.ReactionCustomEmoji) { + return { + documentId: reaction.documentId.toString(), + }; + } + + return undefined; +} + +export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction { + const { + selectAnimation, staticIcon, reaction, title, appearAnimation, + inactive, aroundAnimation, centerIcon, effectAnimation, activateAnimation, + premium, + } = availableReaction; + + return { + selectAnimation: buildApiDocument(selectAnimation), + appearAnimation: buildApiDocument(appearAnimation), + activateAnimation: buildApiDocument(activateAnimation), + effectAnimation: buildApiDocument(effectAnimation), + staticIcon: buildApiDocument(staticIcon), + aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined, + centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined, + reaction: { emoticon: reaction } as ApiReactionEmoji, + title, + isInactive: inactive, + isPremium: premium, + }; +} diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts new file mode 100644 index 000000000..b3c66e499 --- /dev/null +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -0,0 +1,144 @@ +import { Api as GramJs } from '../../../lib/gramjs'; + +import type { + ApiMediaArea, ApiMediaAreaCoordinates, ApiMessage, ApiStealthMode, ApiStoryView, ApiTypeStory, +} from '../../types'; + +import { buildCollectionByCallback } from '../../../util/iteratees'; +import { buildApiPeerId } from './peers'; +import { buildPrivacyRules } from './common'; +import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; +import { buildApiReaction } from './reactions'; + +export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiTypeStory { + if (story instanceof GramJs.StoryItemDeleted) { + return { + id: story.id, + userId, + isDeleted: true, + }; + } + + if (story instanceof GramJs.StoryItemSkipped) { + const { + id, date, expireDate, closeFriends, + } = story; + + return { + id, + userId, + ...(closeFriends && { isForCloseFriends: true }), + date, + expireDate, + }; + } + + const { + edited, pinned, expireDate, id, date, caption, + entities, media, privacy, views, + public: isPublic, noforwards, closeFriends, contacts, selectedContacts, + mediaAreas, sentReaction, + } = story; + + const content: ApiMessage['content'] = { + ...buildMessageMediaContent(media), + }; + + if (caption) { + content.text = buildMessageTextContent(caption, entities); + } + + return { + id, + userId, + date, + expireDate, + content, + ...(isPublic && { isPublic }), + ...(edited && { isEdited: true }), + ...(pinned && { isPinned: true }), + ...(contacts && { isForContacts: true }), + ...(selectedContacts && { isForSelectedContacts: true }), + ...(closeFriends && { isForCloseFriends: true }), + ...(noforwards && { noForwards: true }), + ...(views?.viewsCount && { viewsCount: views.viewsCount }), + ...(views?.reactionsCount && { reactionsCount: views.reactionsCount }), + ...(views?.recentViewers && { + recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')), + }), + ...(privacy && { visibility: buildPrivacyRules(privacy) }), + ...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }), + ...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }), + }; +} + +export function buildApiStoryView(view: GramJs.TypeStoryView): ApiStoryView { + const { + userId, date, reaction, blockedMyStoriesFrom, blocked, + } = view; + return { + userId: userId.toString(), + date, + ...(reaction && { reaction: buildApiReaction(reaction) }), + areStoriesBlocked: blocked || blockedMyStoriesFrom, + isUserBlocked: blocked, + }; +} + +export function buildApiStealthMode(stealthMode: GramJs.TypeStoriesStealthMode): ApiStealthMode { + return { + activeUntil: stealthMode.activeUntilDate, + cooldownUntil: stealthMode.cooldownUntilDate, + }; +} + +function buildApiMediaAreaCoordinates(coordinates: GramJs.TypeMediaAreaCoordinates): ApiMediaAreaCoordinates { + const { + x, y, w, h, rotation, + } = coordinates; + + return { + x, + y, + width: w, + height: h, + rotation, + }; +} + +export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | undefined { + if (area instanceof GramJs.MediaAreaVenue) { + const { geo, title, coordinates } = area; + const point = buildGeoPoint(geo); + + if (!point) return undefined; + + return { + type: 'venue', + coordinates: buildApiMediaAreaCoordinates(coordinates), + geo: point, + title, + }; + } + + if (area instanceof GramJs.MediaAreaGeoPoint) { + const { geo, coordinates } = area; + const point = buildGeoPoint(geo); + + if (!point) return undefined; + + return { + type: 'geoPoint', + coordinates: buildApiMediaAreaCoordinates(coordinates), + geo: point, + }; + } + + return undefined; +} + +export function buildApiUsersStories(userStories: GramJs.UserStories) { + const userId = buildApiPeerId(userStories.userId, 'user'); + + return buildCollectionByCallback(userStories.stories, (story) => [story.id, buildApiStory(userId, story)]); +} diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 2e82b12ac..0440003e6 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -667,11 +667,14 @@ export function buildInputPrivacyRules( switch (visibility) { case 'everybody': - rules.push(new GramJs.InputPrivacyValueAllowAll()); - break; - case 'contacts': { - rules.push(new GramJs.InputPrivacyValueAllowContacts()); + if (visibility === 'contacts') { + rules.push(new GramJs.InputPrivacyValueAllowContacts()); + } + + if (visibility === 'everybody') { + rules.push(new GramJs.InputPrivacyValueAllowAll()); + } const users = deniedUserList?.reduce((acc, { id, accessHash }) => { acc.push(new GramJs.InputPeerUser({ diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 46b712c60..fed8fb08c 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -58,7 +58,7 @@ export { } from './management'; export { - updateProfile, checkUsername, updateUsername, fetchBlockedContacts, blockContact, unblockContact, + updateProfile, checkUsername, updateUsername, fetchBlockedUsers, blockUser, unblockUser, updateProfilePhoto, uploadProfilePhoto, deleteProfilePhotos, fetchWallpapers, uploadWallpaper, fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, fetchWebAuthorizations, terminateWebAuthorization, terminateAllWebAuthorizations, @@ -109,8 +109,4 @@ export { broadcastLocalDbUpdateFull, } from '../localDb'; -export { - fetchAllStories, fetchUserStories, fetchUserPinnedStories, fetchUserStoriesByIds, viewStory, markStoryRead, - deleteStory, toggleStoryPinned, fetchStorySeenBy, fetchStoryLink, fetchStoriesArchive, reportStory, editStoryPrivacy, - toggleStoriesHidden, fetchStoriesMaxIds, -} from './stories'; +export * from './stories'; diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index 44e8772f9..65e09d3cb 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -20,6 +20,8 @@ const MEDIA_ENTITY_TYPES = new Set([ 'msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document', 'videoAvatar', ]); +const JPEG_SIZE_TYPES = new Set(['s', 'm', 'x', 'y', 'w', 'a', 'b', 'c', 'd']); + export default async function downloadMedia( { url, mediaFormat, start, end, isHtmlAllowed, @@ -178,7 +180,11 @@ async function download( mimeType = (entity as GramJs.TypeWebDocument).mimeType; fullSize = (entity as GramJs.TypeWebDocument).size; } else { - mimeType = (entity as GramJs.Document).mimeType; + if (JPEG_SIZE_TYPES.has(sizeType || '')) { + mimeType = 'image/jpeg'; + } else { + mimeType = (entity as GramJs.Document).mimeType; + } fullSize = (entity as GramJs.Document).size.toJSNumber(); } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 713dc77ba..864429935 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -38,12 +38,12 @@ import { buildApiMessage, buildLocalForwardedMessage, buildLocalMessage, - buildWebPage, buildApiSponsoredMessage, - buildApiFormattedText, - buildMessageTextContent, - buildMessageMediaContent, } from '../apiBuilders/messages'; +import { + buildMessageMediaContent, buildMessageTextContent, buildWebPage, +} from '../apiBuilders/messageContent'; +import { buildApiFormattedText } from '../apiBuilders/common'; import { buildApiUser } from '../apiBuilders/users'; import { buildInputEntity, diff --git a/src/api/gramjs/methods/reactions.ts b/src/api/gramjs/methods/reactions.ts index 662347cbe..8989f936d 100644 --- a/src/api/gramjs/methods/reactions.ts +++ b/src/api/gramjs/methods/reactions.ts @@ -6,7 +6,7 @@ import type { ApiChat, ApiReaction } from '../../types'; import { REACTION_LIST_LIMIT, RECENT_REACTIONS_LIMIT, TOP_REACTIONS_LIMIT } from '../../../config'; import { buildInputPeer, buildInputReaction } from '../gramjsBuilders'; import { buildApiUser } from '../apiBuilders/users'; -import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/messages'; +import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/reactions'; import { invokeRequest } from './client'; import localDb from '../localDb'; import { addEntitiesToLocalDb } from '../helpers'; diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 316471a15..532a736d0 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -23,8 +23,7 @@ import { buildApiWallpaper, buildApiWebSession, buildLangPack, buildLangPackString, } from '../apiBuilders/misc'; -import { buildPrivacyRules } from '../apiBuilders/messages'; -import { buildApiPhoto } from '../apiBuilders/common'; +import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common'; import { buildApiUser } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; @@ -224,8 +223,13 @@ export async function uploadWallpaper(file: File) { return { wallpaper }; } -export async function fetchBlockedContacts() { +export async function fetchBlockedUsers({ + isOnlyStories, +}: { + isOnlyStories?: true; +}) { const result = await invokeRequest(new GramJs.contacts.GetBlocked({ + myStoriesFrom: isOnlyStories, limit: BLOCKED_LIST_LIMIT, })); if (!result) { @@ -242,15 +246,29 @@ export async function fetchBlockedContacts() { }; } -export function blockContact(chatOrUserId: string, accessHash?: string) { +export function blockUser({ + user, + isOnlyStories, +} : { + user: ApiUser; + isOnlyStories?: true; +}) { return invokeRequest(new GramJs.contacts.Block({ - id: buildInputPeer(chatOrUserId, accessHash), + id: buildInputPeer(user.id, user.accessHash), + myStoriesFrom: isOnlyStories, })); } -export function unblockContact(chatOrUserId: string, accessHash?: string) { +export function unblockUser({ + user, + isOnlyStories, +} : { + user: ApiUser; + isOnlyStories?: true; +}) { return invokeRequest(new GramJs.contacts.Unblock({ - id: buildInputPeer(chatOrUserId, accessHash), + id: buildInputPeer(user.id, user.accessHash), + myStoriesFrom: isOnlyStories, })); } diff --git a/src/api/gramjs/methods/stories.ts b/src/api/gramjs/methods/stories.ts index 6074e5eaa..820f981dc 100644 --- a/src/api/gramjs/methods/stories.ts +++ b/src/api/gramjs/methods/stories.ts @@ -1,18 +1,21 @@ -import BigInt from 'big-integer'; import { invokeRequest } from './client'; import type { - ApiUser, ApiUserStories, ApiReportReason, ApiTypeStory, + ApiUser, ApiUserStories, ApiReportReason, ApiTypeStory, ApiReaction, ApiStealthMode, } from '../../types'; import type { PrivacyVisibility } from '../../../types'; import { Api as GramJs } from '../../../lib/gramjs'; import { addEntitiesToLocalDb, addStoryToLocalDb } from '../helpers'; import { buildApiUser } from '../apiBuilders/users'; -import { buildApiStory, buildApiUsersStories } from '../apiBuilders/messages'; import { buildApiPeerId } from '../apiBuilders/peers'; import { + buildApiStoryView, buildApiStory, buildApiUsersStories, buildApiStealthMode, +} from '../apiBuilders/stories'; +import { + buildInputEntity, buildInputPeer, buildInputPeerFromLocalDb, buildInputPrivacyRules, + buildInputReaction, buildInputReportReason, } from '../gramjsBuilders'; import { STORY_LIST_LIMIT } from '../../../config'; @@ -28,8 +31,14 @@ export async function fetchAllStories({ isHidden?: boolean; }): Promise< undefined - | { state: string } - | { users: ApiUser[]; userStories: Record; hasMore?: true; state: string }> { + | { state: string; stealthMode: ApiStealthMode } + | { + users: ApiUser[]; + userStories: Record; + hasMore?: true; + state: string; + stealthMode: ApiStealthMode; + }> { const params: ConstructorParameters[0] = isFirstRequest ? (isHidden ? { hidden: true } : {}) : { state: stateHash, next: true, ...(isHidden && { hidden: true }) }; @@ -42,6 +51,7 @@ export async function fetchAllStories({ if (result instanceof GramJs.stories.AllStoriesNotModified) { return { state: result.state, + stealthMode: buildApiStealthMode(result.stealthMode), }; } @@ -95,6 +105,7 @@ export async function fetchAllStories({ userStories: allUserStories, hasMore: result.hasMore, state: result.state, + stealthMode: buildApiStealthMode(result.stealthMode), }; } @@ -215,19 +226,28 @@ export function toggleStoryPinned({ storyId, isPinned }: { storyId: number; isPi return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned })); } -export async function fetchStorySeenBy({ - storyId, limit = STORY_LIST_LIMIT, offsetDate = 0, offsetId = 0, +export async function fetchStoryViewList({ + storyId, + areJustContacts, + query, + areReactionsFirst, + limit = STORY_LIST_LIMIT, + offset = '', }: { storyId: number; + areJustContacts?: true; + areReactionsFirst?: true; + query?: string; limit?: number; - offsetDate?: number; - offsetId?: number; + offset?: string; }) { const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({ id: storyId, + justContacts: areJustContacts, + q: query, + reactionsFirst: areReactionsFirst, limit, - offsetDate, - offsetId: BigInt(offsetId), + offset, })); if (!result) { @@ -236,13 +256,15 @@ export async function fetchStorySeenBy({ addEntitiesToLocalDb(result.users); const users = result.users.map(buildApiUser).filter(Boolean); - const seenByDates = result.views.reduce>((acc, view) => { - acc[buildApiPeerId(view.userId, 'user')] = view.date; + const views = result.views.map(buildApiStoryView); - return acc; - }, {}); - - return { users, seenByDates, count: result.count }; + return { + users, + views, + nextOffset: result.nextOffset, + reactionsCount: result.reactionsCount, + viewsCount: result.count, + }; } export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) { @@ -341,3 +363,36 @@ async function fetchCommonStoriesRequest({ method, userId }: { stories, }; } + +export function sendStoryReaction({ + user, storyId, reaction, shouldAddToRecent, +}: { + user: ApiUser; + 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, + storyId, + ...(shouldAddToRecent && { addToRecent: true }), + }), { + shouldReturnTrue: true, + }); +} + +export function activateStealthMode({ + isForPast, + isForFuture, +}: { + isForPast?: true; + isForFuture?: true; +}) { + return invokeRequest(new GramJs.stories.ActivateStealthMode({ + past: isForPast, + future: isForFuture, + }), { + shouldReturnTrue: true, + }); +} diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index 56e3fe970..5e8f7785b 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -10,7 +10,7 @@ import { } from '../apiBuilders/symbols'; import { buildApiUserEmojiStatus } from '../apiBuilders/users'; import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders'; -import { buildVideoFromDocument } from '../apiBuilders/messages'; +import { buildVideoFromDocument } from '../apiBuilders/messageContent'; import { DEFAULT_GIF_SEARCH_BOT_USERNAME, RECENT_STATUS_LIMIT, RECENT_STICKERS_LIMIT } from '../../../config'; import localDb from '../localDb'; diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 9a2d76d9a..a7eddc83f 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -5,23 +5,33 @@ import type { ApiStory, ApiStorySkipped, } from '../types'; +import localDb from './localDb'; import { DEBUG, GENERAL_TOPIC_ID } from '../../config'; import { omit, pick } from '../../util/iteratees'; import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime'; +import { + addMessageToLocalDb, + addEntitiesToLocalDb, + addPhotoToLocalDb, + resolveMessageApiChatId, + serializeBytes, + log, + swapLocalInvoiceMedia, + isChatFolder, + addStoryToLocalDb, +} from './helpers'; +import { scheduleMutedTopicUpdate, scheduleMutedChatUpdate } from './scheduleUnmute'; import { buildApiMessage, buildApiMessageFromShort, buildApiMessageFromShortChat, - buildMessageMediaContent, - buildPoll, - buildPollResults, buildApiMessageFromNotification, buildMessageDraft, - buildMessageReactions, - buildApiMessageExtendedMediaPreview, - buildApiStory, - buildPrivacyRules, } from './apiBuilders/messages'; +import { + buildApiReaction, + buildMessageReactions, +} from './apiBuilders/reactions'; import { buildChatMember, buildChatMembers, @@ -36,30 +46,20 @@ import { buildApiUserEmojiStatus, buildApiUserStatus, } from './apiBuilders/users'; -import { - buildMessageFromUpdate, - isMessageWithMedia, - buildChatPhotoForLocalDb, -} from './gramjsBuilders'; -import localDb from './localDb'; import { omitVirtualClassFields } from './apiBuilders/helpers'; -import { - addMessageToLocalDb, - addEntitiesToLocalDb, - addPhotoToLocalDb, - resolveMessageApiChatId, - serializeBytes, - log, - swapLocalInvoiceMedia, - isChatFolder, - addStoryToLocalDb, -} from './helpers'; import { buildApiNotifyException, buildApiNotifyExceptionTopic, buildPrivacyKey, } from './apiBuilders/misc'; -import { buildApiPhoto, buildApiUsernames } from './apiBuilders/common'; +import { + buildApiMessageExtendedMediaPreview, + buildMessageMediaContent, + buildPoll, + buildPollResults, +} from './apiBuilders/messageContent'; +import { buildApiStealthMode, buildApiStory } from './apiBuilders/stories'; +import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from './apiBuilders/common'; import { buildApiGroupCall, buildApiGroupCallParticipant, @@ -69,7 +69,11 @@ import { import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols'; import { buildApiBotMenuButton } from './apiBuilders/bots'; -import { scheduleMutedTopicUpdate, scheduleMutedChatUpdate } from './scheduleUnmute'; +import { + buildMessageFromUpdate, + isMessageWithMedia, + buildChatPhotoForLocalDb, +} from './gramjsBuilders'; export type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } @@ -879,6 +883,7 @@ export function updater(update: Update) { '@type': 'updatePeerBlocked', id: getApiChatIdFromMtpPeer(update.peerId), isBlocked: update.blocked, + isBlockedFromStories: update.blockedMyStoriesFrom, }); } else if (update instanceof GramJs.UpdatePrivacy) { const key = buildPrivacyKey(update.key); @@ -1086,6 +1091,18 @@ export function updater(update: Update) { userId: buildApiPeerId(update.userId, 'user'), lastReadId: update.maxId, }); + } else if (update instanceof GramJs.UpdateSentStoryReaction) { + onUpdate({ + '@type': 'updateSentStoryReaction', + userId: buildApiPeerId(update.userId, 'user'), + storyId: update.storyId, + reaction: buildApiReaction(update.reaction), + }); + } else if (update instanceof GramJs.UpdateStoriesStealthMode) { + onUpdate({ + '@type': 'updateStealthMode', + stealthMode: buildApiStealthMode(update.stealthMode), + }); } else if (DEBUG) { const params = typeof update === 'object' && 'className' in update ? update.className : update; log('UNEXPECTED UPDATE', params); diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index ab799c0ae..5427f5dc8 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -212,7 +212,7 @@ export interface ApiPaymentCredentials { title: string; } -interface ApiGeoPoint { +export interface ApiGeoPoint { long: number; lat: number; accessHash: string; diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index 687fe5b8c..2afa3c873 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -1,4 +1,4 @@ -import type { ApiMessage } from './messages'; +import type { ApiGeoPoint, ApiMessage, ApiReaction } from './messages'; import type { ApiPrivacySettings } from '../../types'; export interface ApiStory { @@ -16,8 +16,11 @@ export interface ApiStory { isPublic?: boolean; noForwards?: boolean; viewsCount?: number; + reactionsCount?: number; recentViewerIds?: string[]; visibility?: ApiPrivacySettings; + sentReaction?: ApiReaction; + mediaAreas?: ApiMediaArea[]; } export interface ApiStorySkipped { @@ -57,3 +60,39 @@ export type ApiWebPageStoryData = { id: number; userId: string; }; + +export type ApiStoryView = { + userId: string; + date: number; + reaction?: ApiReaction; + isUserBlocked?: true; + areStoriesBlocked?: true; +}; + +export type ApiStealthMode = { + activeUntil?: number; + cooldownUntil?: number; +}; + +export type ApiMediaAreaCoordinates = { + x: number; + y: number; + width: number; + height: number; + rotation: number; +}; + +export type ApiMediaAreaVenue = { + type: 'venue'; + coordinates: ApiMediaAreaCoordinates; + geo: ApiGeoPoint; + title: string; +}; + +export type ApiMediaAreaGeoPoint = { + type: 'geoPoint'; + coordinates: ApiMediaAreaCoordinates; + geo: ApiGeoPoint; +}; + +export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 8f3f8c69a..2537d1b3c 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -18,6 +18,7 @@ import type { ApiMessageExtendedMediaPreview, ApiPhoto, ApiPoll, + ApiReaction, ApiReactions, ApiStickerSet, ApiThreadInfo, @@ -33,7 +34,7 @@ import type { } from './calls'; import type { ApiBotMenuButton } from './bots'; import type { ApiPrivacyKey, PrivacyVisibility } from '../../types'; -import type { ApiStory, ApiStorySkipped } from './stories'; +import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; export type ApiUpdateReady = { '@type': 'updateApiReady'; @@ -462,7 +463,8 @@ export type ApiUpdateTwoFaStateWaitCode = { export type ApiUpdatePeerBlocked = { '@type': 'updatePeerBlocked'; id: string; - isBlocked: boolean; + isBlocked?: boolean; + isBlockedFromStories?: boolean; }; export type ApiUpdatePaymentVerificationNeeded = { @@ -643,6 +645,18 @@ export type ApiUpdateReadStories = { lastReadId: number; }; +export type ApiUpdateSentStoryReaction = { + '@type': 'updateSentStoryReaction'; + userId: string; + storyId: number; + reaction?: ApiReaction; +}; + +export type ApiUpdateStealthMode = { + '@type': 'updateStealthMode'; + stealthMode: ApiStealthMode; +}; + export type ApiRequestSync = { '@type': 'requestSync'; }; @@ -673,8 +687,9 @@ export type ApiUpdate = ( ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic | ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | - ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | - ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages + ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction | + ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | + ApiUpdateStealthMode ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 41442d299..51b714884 100644 Binary files a/src/assets/fonts/icomoon.woff and b/src/assets/fonts/icomoon.woff differ diff --git a/src/assets/fonts/icomoon.woff2 b/src/assets/fonts/icomoon.woff2 index a26a91b20..cff2fdbdd 100644 Binary files a/src/assets/fonts/icomoon.woff2 and b/src/assets/fonts/icomoon.woff2 differ diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 4da45b4c1..a6ef5fecf 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -7,6 +7,7 @@ export { default as AttachBotRecipientPicker } from '../components/main/AttachBo export { default as Dialogs } from '../components/main/Dialogs'; export { default as Notifications } from '../components/main/Notifications'; export { default as SafeLinkModal } from '../components/main/SafeLinkModal'; +export { default as MapModal } from '../components/modals/mapModal/MapModal'; export { default as UrlAuthModal } from '../components/main/UrlAuthModal'; export { default as HistoryCalendar } from '../components/main/HistoryCalendar'; export { default as NewContactModal } from '../components/main/NewContactModal'; diff --git a/src/components/calls/group/GroupCallParticipantVideo.tsx b/src/components/calls/group/GroupCallParticipantVideo.tsx index 7a23a5097..cee19a2be 100644 --- a/src/components/calls/group/GroupCallParticipantVideo.tsx +++ b/src/components/calls/group/GroupCallParticipantVideo.tsx @@ -29,7 +29,7 @@ import Button from '../../ui/Button'; import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon'; import FullNameTitle from '../../common/FullNameTitle'; import GroupCallParticipantMenu from './GroupCallParticipantMenu'; -import Skeleton from '../../ui/Skeleton'; +import Skeleton from '../../ui/placeholder/Skeleton'; import styles from './GroupCallParticipantVideo.module.scss'; diff --git a/src/components/calls/group/MicrophoneButton.tsx b/src/components/calls/group/MicrophoneButton.tsx index 6dcbebd4f..28550e911 100644 --- a/src/components/calls/group/MicrophoneButton.tsx +++ b/src/components/calls/group/MicrophoneButton.tsx @@ -151,8 +151,7 @@ const MicrophoneButton: FC = ({ play={playSegment.toString()} playSegment={playSegment} className={styles.icon} - forceOnHeavyAnimation - forceInBackground + forceAlways /> diff --git a/src/components/calls/group/OutlinedMicrophoneIcon.tsx b/src/components/calls/group/OutlinedMicrophoneIcon.tsx index de6fd67d8..f0e5d85d9 100644 --- a/src/components/calls/group/OutlinedMicrophoneIcon.tsx +++ b/src/components/calls/group/OutlinedMicrophoneIcon.tsx @@ -81,8 +81,7 @@ const OutlinedMicrophoneIcon: FC = ({ size={28} color={microphoneColor} className={className} - forceOnHeavyAnimation - forceInBackground + forceAlways nonInteractive /> ); diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index da196f0d3..cbf38afbd 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -41,8 +41,8 @@ export type OwnProps = { quality?: number; color?: string; isLowPriority?: boolean; + forceAlways?: boolean; forceOnHeavyAnimation?: boolean; - forceInBackground?: boolean; sharedCanvas?: HTMLCanvasElement; sharedCanvasCoords?: { x: number; y: number }; onClick?: NoneToVoidFunction; @@ -67,8 +67,8 @@ const AnimatedSticker: FC = ({ quality, isLowPriority, color, + forceAlways, forceOnHeavyAnimation, - forceInBackground, sharedCanvas, sharedCanvasCoords, onClick, @@ -181,7 +181,7 @@ const AnimatedSticker: FC = ({ if ( !animation || !(playRef.current || playSegmentRef.current) - || isFrozen(forceOnHeavyAnimation, forceInBackground) + || isFrozen(forceAlways) ) { return; } @@ -221,13 +221,13 @@ const AnimatedSticker: FC = ({ } if (playKey) { - if (!isFrozen(forceOnHeavyAnimation, forceInBackground)) { + if (!isFrozen(forceAlways, forceOnHeavyAnimation)) { playAnimation(noLoop); } } else { pauseAnimation(); } - }, [animation, playKey, noLoop, playAnimation, pauseAnimation, forceOnHeavyAnimation, forceInBackground]); + }, [animation, playKey, noLoop, playAnimation, pauseAnimation, forceAlways, forceOnHeavyAnimation]); useEffect(() => { if (animation) { @@ -240,12 +240,12 @@ const AnimatedSticker: FC = ({ } }, [playAnimation, animation, tgsUrl]); - useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || forceOnHeavyAnimation); - usePriorityPlaybackCheck(pauseAnimation, playAnimation, !playKey); + useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || forceAlways || forceOnHeavyAnimation); + usePriorityPlaybackCheck(pauseAnimation, playAnimation, !playKey || forceAlways); // Pausing frame may not happen in background, // so we need to make sure it happens right after focusing, // then we can play again. - useBackgroundMode(pauseAnimation, playAnimationOnRaf, !playKey || forceInBackground); + useBackgroundMode(pauseAnimation, playAnimationOnRaf, !playKey || forceAlways); if (sharedCanvas) { return undefined; @@ -268,8 +268,7 @@ const AnimatedSticker: FC = ({ export default memo(AnimatedSticker); -function isFrozen(forceOnHeavyAnimation = false, forceInBackground = false) { - return (!forceOnHeavyAnimation && isHeavyAnimating()) - || isPriorityPlaybackActive() - || (!forceInBackground && isBackgroundModeActive()); +function isFrozen(forceAlways = false, forceOnHeavyAnimation = false) { + if (forceAlways) return false; + return (!forceOnHeavyAnimation && isHeavyAnimating()) || isPriorityPlaybackActive() || isBackgroundModeActive(); } diff --git a/src/components/common/Avatar.scss b/src/components/common/Avatar.scss index 71555d30b..e6589bc0e 100644 --- a/src/components/common/Avatar.scss +++ b/src/components/common/Avatar.scss @@ -118,6 +118,17 @@ } } + &.size-giant { + width: 5rem; + height: 5rem; + font-size: 2.5rem; + + .emoji { + width: 2.5rem; + height: 2.5rem; + } + } + &.size-jumbo { width: 7.5rem; height: 7.5rem; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 57d902794..f051c03a8 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -36,7 +36,7 @@ import './Avatar.scss'; const LOOP_COUNT = 3; -export type AvatarSize = 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'jumbo'; +export type AvatarSize = 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'giant' | 'jumbo'; const cn = createClassNameBuilder('Avatar'); cn.media = cn('media'); @@ -51,6 +51,7 @@ type OwnProps = { isSavedMessages?: boolean; withVideo?: boolean; withStory?: boolean; + forPremiumPromo?: boolean; withStoryGap?: boolean; withStorySolid?: boolean; storyViewerOrigin?: StoryViewerOrigin; @@ -70,6 +71,7 @@ const Avatar: FC = ({ isSavedMessages, withVideo, withStory, + forPremiumPromo, withStoryGap, withStorySolid, storyViewerOrigin, @@ -211,7 +213,7 @@ const Avatar: FC = ({ isDeleted && 'deleted-account', isReplies && 'replies-bot-account', isForum && 'forum', - withStory && user?.hasStories && 'with-story-circle', + ((withStory && user?.hasStories) || forPremiumPromo) && 'with-story-circle', withStorySolid && user?.hasStories && 'with-story-solid', withStorySolid && user?.hasUnreadStories && 'has-unread-story', onClick && 'interactive', diff --git a/src/components/common/AvatarStoryCircle.tsx b/src/components/common/AvatarStoryCircle.tsx index 049849aad..b6cef93c8 100644 --- a/src/components/common/AvatarStoryCircle.tsx +++ b/src/components/common/AvatarStoryCircle.tsx @@ -34,11 +34,13 @@ const SIZES: Record = { 'small-mobile': 2.625 * DPR * REM, medium: 2.875 * DPR * REM, large: 3.5 * DPR * REM, + giant: 5.125 * DPR * REM, jumbo: 7.625 * DPR * REM, }; const BLUE = ['#34C578', '#3CA3F3']; const GREEN = ['#C9EB38', '#09C167']; +const PURPLE = ['#A667FF', '#55A5FF']; const GRAY = '#C4C9CC'; const DARK_GRAY = '#737373'; const STROKE_WIDTH = 0.125 * DPR * REM; @@ -119,7 +121,7 @@ export default memo(withGlobal((global, { userId }): StateProps => { }; })(AvatarStoryCircle)); -function drawGradientCircle({ +export function drawGradientCircle({ canvas, size, color, @@ -142,6 +144,8 @@ function drawGradientCircle({ segmentsCount = SEGMENTS_MAX; } + const strokeModifier = Math.max(Math.max(size - SIZES.large, 0) / DPR / REM / 1.5, 1); + const ctx = canvas.getContext('2d'); if (!ctx) { return; @@ -150,7 +154,7 @@ function drawGradientCircle({ canvas.width = size; canvas.height = size; const centerCoordinate = size / 2; - const radius = (size - STROKE_WIDTH) / 2; + const radius = (size - STROKE_WIDTH * strokeModifier) / 2; const segmentAngle = (2 * Math.PI) / segmentsCount; const gapSize = (GAP_PERCENT / 100) * (2 * Math.PI); const gradient = ctx.createLinearGradient( @@ -160,7 +164,7 @@ function drawGradientCircle({ Math.ceil(size * Math.sin(Math.PI / 2)), ); - const colorStops = color === 'green' ? GREEN : BLUE; + const colorStops = color === 'purple' ? PURPLE : color === 'green' ? GREEN : BLUE; colorStops.forEach((colorStop, index) => { gradient.addColorStop(index / (colorStops.length - 1), colorStop); }); @@ -174,7 +178,7 @@ function drawGradientCircle({ let endAngle = startAngle + segmentAngle - (segmentsCount > 1 ? gapSize : 0); ctx.strokeStyle = isRead ? readSegmentColor : gradient; - ctx.lineWidth = isRead ? STROKE_WIDTH_READ : STROKE_WIDTH; + ctx.lineWidth = (isRead ? STROKE_WIDTH_READ : STROKE_WIDTH) * strokeModifier; if (withExtraGap) { if (startAngle >= EXTRA_GAP_START && endAngle <= EXTRA_GAP_END) { // Segment is inside extra gap diff --git a/src/components/common/Composer.scss b/src/components/common/Composer.scss index 607fa1618..e358c852a 100644 --- a/src/components/common/Composer.scss +++ b/src/components/common/Composer.scss @@ -427,6 +427,14 @@ right: 0.5rem; } } + + &.story-reaction-button { + --custom-emoji-size: 1.5rem; + } + + .story-reaction-heart { + color: var(--color-heart); + } } > .input-group { @@ -718,6 +726,11 @@ } } +#story-input-text .placeholder-text { + bottom: 0.875rem; + left: 0.875rem; +} + .composer-tooltip { position: absolute; bottom: calc(100% + 0.5rem); diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index b386e3383..22d4f3fa6 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -24,6 +24,7 @@ import type { ApiMessageEntity, ApiNewPoll, ApiReaction, + ApiStealthMode, ApiSticker, ApiUser, ApiVideo, @@ -68,6 +69,7 @@ import { selectTheme, selectUser, selectUserFullInfo, + selectUserStory, } from '../../global/selectors'; import { getAllowedAttachmentOptions, @@ -146,6 +148,7 @@ import StickerTooltip from '../middle/composer/StickerTooltip.async'; import EmojiTooltip from '../middle/composer/EmojiTooltip.async'; import CustomSendMenu from '../middle/composer/CustomSendMenu.async'; import ReactionSelector from '../middle/message/ReactionSelector'; +import ReactionStaticEmoji from './ReactionStaticEmoji'; import './Composer.scss'; @@ -230,6 +233,8 @@ type StateProps = canPlayAnimatedEmojis?: boolean; canBuyPremium?: boolean; shouldCollectDebugLogs?: boolean; + sentStoryReaction?: ApiReaction; + stealthMode?: ApiStealthMode; }; enum MainButtonState { @@ -254,6 +259,10 @@ const MESSAGE_MAX_LENGTH = 4096; const SENDING_ANIMATION_DURATION = 350; const MOUNT_ANIMATION_DURATION = 430; +const HEART_REACTION: ApiReaction = { + emoticon: '❤', +}; + const Composer: FC = ({ type, isOnActiveTab, @@ -328,6 +337,8 @@ const Composer: FC = ({ canBuyPremium, canPlayAnimatedEmojis, shouldCollectDebugLogs, + sentStoryReaction, + stealthMode, onForward, }) => { const { @@ -349,6 +360,7 @@ const Composer: FC = ({ showAllowedMessageTypesNotification, openStoryReactionPicker, closeReactionPicker, + sendStoryReaction, } = getActions(); const lang = useLang(); @@ -356,6 +368,9 @@ const Composer: FC = ({ // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const storyReactionRef = useRef(null); + const [getHtml, setHtml] = useSignal(''); const [isMounted, setIsMounted] = useState(false); const getSelectionRange = useGetSelectionRange(editableInputCssSelector); @@ -374,6 +389,9 @@ const Composer: FC = ({ const [isInputHasFocus, markInputHasFocus, unmarkInputHasFocus] = useFlag(); const [isAttachMenuOpen, onAttachMenuOpen, onAttachMenuClose] = useFlag(); + const isSentStoryReactionHeart = sentStoryReaction && 'emoticon' in sentStoryReaction + ? sentStoryReaction.emoticon === HEART_REACTION.emoticon : false; + useEffect(processMessageInputForCustomEmoji, [getHtml]); const customEmojiNotificationNumber = useRef(0); @@ -398,10 +416,10 @@ const Composer: FC = ({ }, [chatId]); useEffect(() => { - if (chatId && isReady) { + if (chatId && isReady && !isInStoryViewer) { loadScheduledHistory({ chatId }); } - }, [isReady, chatId, threadId]); + }, [isReady, chatId, threadId, isInStoryViewer]); useEffect(() => { if (chatId && chat && !sendAsPeerIds && isReady && isChatSuperGroup(chat)) { @@ -521,22 +539,25 @@ const Composer: FC = ({ startRecordTimeRef, } = useVoiceRecording(); + const shouldSendRecordingStatus = isForCurrentMessageList && !isInStoryViewer; useInterval(() => { sendMessageAction({ type: 'recordAudio' }); - }, activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL); + }, shouldSendRecordingStatus ? activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL : undefined); useEffect(() => { + if (!isForCurrentMessageList || isInStoryViewer) return; if (!activeVoiceRecording) { sendMessageAction({ type: 'cancel' }); } - }, [activeVoiceRecording, sendMessageAction]); + }, [activeVoiceRecording, isForCurrentMessageList, isInStoryViewer, sendMessageAction]); const isEditingRef = useStateRef(Boolean(editingMessage)); useEffect(() => { + if (!isForCurrentMessageList || isInStoryViewer) return; if (getHtml() && !isEditingRef.current) { sendMessageAction({ type: 'typing' }); } - }, [getHtml, isEditingRef, sendMessageAction]); + }, [getHtml, isEditingRef, isForCurrentMessageList, isInStoryViewer, sendMessageAction]); const isAdmin = chat && isChatAdmin(chat); @@ -736,8 +757,28 @@ const Composer: FC = ({ handleContextMenuHide, } = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu)); + const { + contextMenuPosition: storyReactionPickerPosition, + handleContextMenu: handleStoryPickerContextMenu, + handleBeforeContextMenu: handleBeforeStoryPickerContextMenu, + handleContextMenuHide: handleStoryPickerContextMenuHide, + } = useContextMenuHandlers(storyReactionRef, !isInStoryViewer); + + useEffect(() => { + if (isReactionPickerOpen) return; + + if (storyReactionPickerPosition) { + openStoryReactionPicker({ + storyUserId: chatId, + storyId: storyId!, + position: storyReactionPickerPosition, + }); + handleStoryPickerContextMenuHide(); + } + }, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerPosition]); + useClipboardPaste( - isForCurrentMessageList, + isForCurrentMessageList || isInStoryViewer, insertFormattedTextAndUpdateCursor, handleSetAttachments, setNextText, @@ -1226,6 +1267,18 @@ const Composer: FC = ({ return withBotMenuButton && !getHtml() && !activeVoiceRecording; }, [withBotMenuButton, getHtml, activeVoiceRecording]); + const [timedPlaceholderLangKey, timedPlaceholderDate] = useMemo(() => { + if (slowMode?.nextSendDate) { + return ['SlowModeWait', slowMode.nextSendDate]; + } + + if (stealthMode?.activeUntil && isInStoryViewer) { + return ['StealthModeActiveHint', stealthMode.activeUntil]; + } + + return []; + }, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]); + const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen || isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen || isStickerTooltipOpen || isBotCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen @@ -1348,7 +1401,17 @@ const Composer: FC = ({ }); const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => { - openStoryReactionPicker({ storyUserId: chatId, storyId: storyId!, position }); + openStoryReactionPicker({ + storyUserId: chatId, + storyId: storyId!, + position, + sendAsMessage: true, + }); + }); + + const handleLikeStory = useLastCallback(() => { + const reaction = sentStoryReaction ? undefined : HEART_REACTION; + sendStoryReaction({ userId: chatId, storyId: storyId!, reaction }); }); const handleSendScheduled = useLastCallback(() => { @@ -1586,6 +1649,7 @@ const Composer: FC = ({ canSendPlainText={!isComposerBlocked} inputCssSelector={editableInputCssSelector} idPrefix={type} + forceDarkTheme={isInStoryViewer} /> )} = ({ ? (botKeyboardPlaceholder || inputPlaceholder || lang('Message')) : lang('Chat.PlaceholderTextNotAllowed')) } + timedPlaceholderDate={timedPlaceholderDate} + timedPlaceholderLangKey={timedPlaceholderLangKey} forcedPlaceholder={inlineBotHelp} canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList} noFocusInterception={hasAttachments} @@ -1618,6 +1684,33 @@ const Composer: FC = ({ onFocus={markInputHasFocus} onBlur={unmarkInputHasFocus} /> + {isInStoryViewer && !activeVoiceRecording && ( + + )} {isInMessageList && ( <> {isInlineBotLoading && Boolean(inlineBotId) && ( @@ -1730,7 +1823,7 @@ const Composer: FC = ({ )} @@ -321,6 +333,7 @@ const StickerSet: FC = ({ onClick={onReactionSelect!} sharedCanvasRef={sharedCanvasRef} sharedCanvasHqRef={sharedCanvasHqRef} + forcePlayback={forcePlayback} /> ); })} @@ -358,6 +371,7 @@ const StickerSet: FC = ({ onContextMenuOpen={onContextMenuOpen} onContextMenuClose={onContextMenuClose} onContextMenuClick={onContextMenuClick} + forcePlayback={forcePlayback} /> ); })} diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index c24285c4d..dc46b77ce 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -38,6 +38,7 @@ type OwnProps = { loopLimit?: number; shouldLoop?: boolean; shouldPreloadPreview?: boolean; + forceAlways?: boolean; forceOnHeavyAnimation?: boolean; observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; @@ -65,6 +66,7 @@ const StickerView: FC = ({ loopLimit, shouldLoop = false, shouldPreloadPreview, + forceAlways, forceOnHeavyAnimation, observeIntersectionForLoading, observeIntersectionForPlaying, @@ -161,7 +163,8 @@ const StickerView: FC = ({ tgsUrl={fullMediaData} play={shouldPlay} noLoop={!shouldLoop} - forceOnHeavyAnimation={forceOnHeavyAnimation} + forceOnHeavyAnimation={forceAlways || forceOnHeavyAnimation} + forceAlways={forceAlways} isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)} sharedCanvas={sharedCanvasRef?.current || undefined} sharedCanvasCoords={coords} @@ -178,6 +181,7 @@ const StickerView: FC = ({ playsInline muted loop={!loopLimit} + isPriority={forceAlways} disablePictureInPicture onReady={markPlayerReady} onEnded={onVideoEnded} diff --git a/src/components/common/helpers/renderMessageText.ts b/src/components/common/helpers/renderMessageText.ts index a6da21d8c..964873ee6 100644 --- a/src/components/common/helpers/renderMessageText.ts +++ b/src/components/common/helpers/renderMessageText.ts @@ -14,15 +14,25 @@ import renderText from './renderText'; import { renderTextWithEntities } from './renderTextWithEntities'; import trimText from '../../../util/trimText'; -export function renderMessageText( - message: ApiMessage, - highlight?: string, - emojiSize?: number, - isSimple?: boolean, - truncateLength?: number, - isProtected?: boolean, - shouldRenderAsHtml?: boolean, -) { +export function renderMessageText({ + message, + highlight, + emojiSize, + isSimple, + truncateLength, + isProtected, + forcePlayback, + shouldRenderAsHtml, +} : { + message: ApiMessage; + highlight?: string; + emojiSize?: number; + isSimple?: boolean; + truncateLength?: number; + isProtected?: boolean; + forcePlayback?: boolean; + shouldRenderAsHtml?: boolean; +}) { const { text, entities } = message.content.text || {}; if (!text) { @@ -39,6 +49,7 @@ export function renderMessageText( messageId: message.id, isSimple, isProtected, + forcePlayback, }); } @@ -67,7 +78,9 @@ export function renderMessageSummary( const emoji = !noEmoji && getMessageSummaryEmoji(message); const emojiWithSpace = emoji ? `${emoji} ` : ''; - const text = renderMessageText(message, highlight, undefined, true, truncateLength); + const text = renderMessageText({ + message, highlight, isSimple: true, truncateLength, + }); const description = getMessageSummaryDescription(lang, message, text); return [ diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index 69a446186..2fbdf767f 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -42,6 +42,7 @@ export function renderTextWithEntities({ sharedCanvasRef, sharedCanvasHqRef, cacheBuster, + forcePlayback, }: { text: string; entities?: ApiMessageEntity[]; @@ -57,8 +58,9 @@ export function renderTextWithEntities({ sharedCanvasRef?: React.RefObject; sharedCanvasHqRef?: React.RefObject; cacheBuster?: string; + forcePlayback?: boolean; }) { - if (!entities || !entities.length) { + if (!entities?.length) { return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple); } @@ -131,7 +133,7 @@ export function renderTextWithEntities({ // Render the entity itself const newEntity = shouldRenderAsHtml ? processEntityAsHtml(entity, entityContent, nestedEntityContent) - : processEntity( + : processEntity({ entity, entityContent, nestedEntityContent, @@ -146,7 +148,8 @@ export function renderTextWithEntities({ sharedCanvasRef, sharedCanvasHqRef, cacheBuster, - ); + forcePlayback, + }); if (Array.isArray(newEntity)) { renderResult.push(...newEntity); @@ -312,22 +315,39 @@ function organizeEntity( }; } -function processEntity( - entity: ApiMessageEntity, - entityContent: TextPart, - nestedEntityContent: TextPart[], - highlight?: string, - messageId?: number, - isSimple?: boolean, - isProtected?: boolean, - observeIntersectionForLoading?: ObserveFn, - observeIntersectionForPlaying?: ObserveFn, - withTranslucentThumbs?: boolean, - emojiSize?: number, - sharedCanvasRef?: React.RefObject, - sharedCanvasHqRef?: React.RefObject, - cacheBuster?: string, -) { +function processEntity({ + entity, + entityContent, + nestedEntityContent, + highlight, + messageId, + isSimple, + isProtected, + observeIntersectionForLoading, + observeIntersectionForPlaying, + withTranslucentThumbs, + emojiSize, + sharedCanvasRef, + sharedCanvasHqRef, + cacheBuster, + forcePlayback, +} : { + entity: ApiMessageEntity; + entityContent: TextPart; + nestedEntityContent: TextPart[]; + highlight?: string; + messageId?: number; + isSimple?: boolean; + isProtected?: boolean; + observeIntersectionForLoading?: ObserveFn; + observeIntersectionForPlaying?: ObserveFn; + withTranslucentThumbs?: boolean; + emojiSize?: number; + sharedCanvasRef?: React.RefObject; + sharedCanvasHqRef?: React.RefObject; + cacheBuster?: string; + forcePlayback?: boolean; +}) { const entityText = typeof entityContent === 'string' && entityContent; const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent; @@ -360,6 +380,7 @@ function processEntity( observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForPlaying={observeIntersectionForPlaying} withTranslucentThumb={withTranslucentThumbs} + forceAlways={forcePlayback} /> ); } @@ -485,6 +506,7 @@ function processEntity( observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForPlaying={observeIntersectionForPlaying} withTranslucentThumb={withTranslucentThumbs} + forceAlways={forcePlayback} /> ); default: diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index 1559f1c45..cd6b89ab3 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -319,11 +319,11 @@ } } - .colon, .forward { + .colon, .chat-prefix-icon { margin-inline-end: 0.1875rem; } - .forward { + .chat-prefix-icon { display: inline-block; color: var(--color-list-icon); font-size: 0.875rem; diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index c74d9dc12..c499a32ae 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -151,7 +151,8 @@ export default function useChatListEntry({ : )} - {lastMessage.forwardInfo && ()} + {lastMessage.forwardInfo && ()} + {Boolean(lastMessage.replyToStoryId) && ()} {renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}

); diff --git a/src/components/left/settings/BlockUserModal.tsx b/src/components/left/settings/BlockUserModal.tsx index 68b555ae0..1e7d4202d 100644 --- a/src/components/left/settings/BlockUserModal.tsx +++ b/src/components/left/settings/BlockUserModal.tsx @@ -37,7 +37,7 @@ const BlockUserModal: FC = ({ }) => { const { setUserSearchQuery, - blockContact, + blockUser, } = getActions(); const lang = useLang(); @@ -65,13 +65,9 @@ const BlockUserModal: FC = ({ }, [blockedIds, contactIds, currentUserId, search, localContactIds, usersById]); const handleRemoveUser = useCallback((userId: string) => { - const { id: contactId, accessHash } = usersById[userId] || {}; - if (!contactId || !accessHash) { - return; - } - blockContact({ contactId, accessHash }); + blockUser({ userId }); onClose(); - }, [blockContact, onClose, usersById]); + }, [onClose]); return ( = ({ {session.domain}, {session.browser}, {session.platform} - {session.ip} {session.region} + {session.ip} {session.region} ); diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 5decab197..1774424a5 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -67,7 +67,7 @@ const SettingsPrivacy: FC = ({ }) => { const { loadPrivacySettings, - loadBlockedContacts, + loadBlockedUsers, loadAuthorizations, loadContentSettings, updateContentSettings, @@ -79,12 +79,12 @@ const SettingsPrivacy: FC = ({ } = getActions(); useEffect(() => { - loadBlockedContacts(); + loadBlockedUsers(); loadAuthorizations(); loadPrivacySettings(); loadContentSettings(); loadWebAuthorizations(); - }, [loadBlockedContacts, loadAuthorizations, loadPrivacySettings, loadContentSettings, loadWebAuthorizations]); + }, []); useEffect(() => { if (isActive) { diff --git a/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx b/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx index f29586e19..76110de74 100644 --- a/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx +++ b/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx @@ -39,13 +39,13 @@ const SettingsPrivacyBlockedUsers: FC = ({ blockedIds, phoneCodeList, }) => { - const { unblockContact } = getActions(); + const { unblockUser } = getActions(); const lang = useLang(); const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag(); - const handleUnblockClick = useCallback((contactId: string) => { - unblockContact({ contactId }); - }, [unblockContact]); + const handleUnblockClick = useCallback((userId: string) => { + unblockUser({ userId }); + }, [unblockUser]); useHistoryBack({ isActive, @@ -53,13 +53,13 @@ const SettingsPrivacyBlockedUsers: FC = ({ }); const blockedUsernamesById = useMemo(() => { - return blockedIds.reduce((acc, contactId) => { - const isPrivate = isUserId(contactId); - const user = isPrivate ? usersByIds[contactId] : undefined; + return blockedIds.reduce((acc, userId) => { + const isPrivate = isUserId(userId); + const user = isPrivate ? usersByIds[userId] : undefined; const mainUsername = user && !user.phoneNumber && getMainUsername(user); if (mainUsername) { - acc[contactId] = mainUsername; + acc[userId] = mainUsername; } return acc; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 45d2727d2..b5ce2245e 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -11,6 +11,7 @@ import type { ApiAttachBot, ApiChat, ApiChatFolder, + ApiGeoPoint, ApiMessage, ApiUser, } from '../../api/types'; @@ -95,6 +96,7 @@ import AttachBotRecipientPicker from './AttachBotRecipientPicker.async'; import ReactionPicker from '../middle/message/ReactionPicker.async'; import ChatlistModal from '../modals/chatlist/ChatlistModal.async'; import StoryViewer from '../story/StoryViewer.async'; +import MapModal from '../modals/mapModal/MapModal.async'; import './Main.scss'; @@ -115,6 +117,8 @@ type StateProps = { hasDialogs: boolean; audioMessage?: ApiMessage; safeLinkModalUrl?: string; + mapModalGeoPoint?: ApiGeoPoint; + mapModalZoom?: number; isHistoryCalendarOpen: boolean; shouldSkipHistoryAnimations?: boolean; openedStickerSetShortName?: string; @@ -171,6 +175,8 @@ const Main: FC = ({ audioMessage, activeGroupCallId, safeLinkModalUrl, + mapModalGeoPoint, + mapModalZoom, isHistoryCalendarOpen, shouldSkipHistoryAnimations, limitReached, @@ -245,6 +251,7 @@ const Main: FC = ({ loadRecentReactions, loadFeaturedEmojiStickers, setIsAppUpdateAvailable, + loadPremiumSetStickers, } = getActions(); if (DEBUG && !DEBUG_isLogged) { @@ -326,6 +333,7 @@ const Main: FC = ({ if (isMasterTab && isCurrentUserPremium) { loadDefaultStatusIcons(); loadRecentEmojiStatuses(); + loadPremiumSetStickers(); } }, [isCurrentUserPremium, isMasterTab]); @@ -517,6 +525,7 @@ const Main: FC = ({ {audioMessage && } + ( urlAuth, webApp, safeLinkModalUrl, + mapModal, openedStickerSetShortName, openedCustomEmojiSetIds, shouldSkipHistoryAnimations, @@ -623,6 +633,8 @@ export default memo(withGlobal( hasDialogs: Boolean(dialogs.length), audioMessage, safeLinkModalUrl, + mapModalGeoPoint: mapModal?.point, + mapModalZoom: mapModal?.zoom, isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt), shouldSkipHistoryAnimations, openedStickerSetShortName, diff --git a/src/components/main/premium/PremiumFeatureItem.module.scss b/src/components/main/premium/PremiumFeatureItem.module.scss index 6fcfb029d..ff312dcfa 100644 --- a/src/components/main/premium/PremiumFeatureItem.module.scss +++ b/src/components/main/premium/PremiumFeatureItem.module.scss @@ -18,7 +18,6 @@ color: var(--color-text-secondary); white-space: pre-wrap; line-height: 1.25rem; - min-height: 2.5rem; } .icon { @@ -29,3 +28,11 @@ background: var(--item-color, #000); margin-left: 1rem; } + +.font-icon { + font-size: 2rem !important; + align-self: center; + margin-right: 0 !important; + margin-left: 1rem; + color: var(--item-color, #000) !important; +} diff --git a/src/components/main/premium/PremiumFeatureItem.tsx b/src/components/main/premium/PremiumFeatureItem.tsx index ebe2960eb..84db291d6 100644 --- a/src/components/main/premium/PremiumFeatureItem.tsx +++ b/src/components/main/premium/PremiumFeatureItem.tsx @@ -3,6 +3,7 @@ import React, { memo } from '../../../lib/teact/teact'; import renderText from '../../common/helpers/renderText'; import { hexToRgb, lerpRgb } from '../../../util/switchTheme'; +import buildClassName from '../../../util/buildClassName'; import ListItem from '../../ui/ListItem'; @@ -10,11 +11,12 @@ import styles from './PremiumFeatureItem.module.scss'; type OwnProps = { icon: string; + isFontIcon?: boolean; title: string; text: string; - onClick: VoidFunction; index: number; count: number; + onClick?: VoidFunction; }; const COLORS = [ @@ -24,6 +26,7 @@ const COLORS = [ const PremiumFeatureItem: FC = ({ icon, + isFontIcon, title, text, index, @@ -33,11 +36,19 @@ const PremiumFeatureItem: FC = ({ const newIndex = (index / count) * COLORS.length; const colorA = COLORS[Math.floor(newIndex)]; const colorB = COLORS[Math.ceil(newIndex)] ?? colorA; - const { r, g, b } = lerpRgb(colorA, colorB, 1); + const { r, g, b } = lerpRgb(colorA, colorB, 0.5); return ( - - + + {isFontIcon ? ( + + ) : ( + + )}
{renderText(title, ['br'])}
{text}
diff --git a/src/components/main/premium/PremiumFeatureModal.module.scss b/src/components/main/premium/PremiumFeatureModal.module.scss index 73f582ac3..9475b3dfa 100644 --- a/src/components/main/premium/PremiumFeatureModal.module.scss +++ b/src/components/main/premium/PremiumFeatureModal.module.scss @@ -52,7 +52,7 @@ margin-bottom: 7.5rem; } -.limits { +.limits, .stories { background: var(--color-background); } @@ -63,6 +63,10 @@ height: calc(var(--vh) * 55 + 41px); } +.stories { + height: calc(var(--vh) * 55 + 100px); +} + .header { padding-left: 4rem; font-size: 1.25rem; @@ -140,4 +144,8 @@ .limits-content { height: calc(var(--vh) * 100 - 193px); } + + .stories { + height: calc(var(--vh) * 100 - 135px); + } } diff --git a/src/components/main/premium/PremiumFeatureModal.tsx b/src/components/main/premium/PremiumFeatureModal.tsx index 70c4644b2..5c5e490b2 100644 --- a/src/components/main/premium/PremiumFeatureModal.tsx +++ b/src/components/main/premium/PremiumFeatureModal.tsx @@ -19,6 +19,7 @@ import PremiumLimitPreview from './common/PremiumLimitPreview'; import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo'; import SliderDots from '../../common/SliderDots'; import PremiumFeaturePreviewStickers from './previews/PremiumFeaturePreviewStickers'; +import PremiumFeaturePreviewStories from './previews/PremiumFeaturePreviewStories'; import styles from './PremiumFeatureModal.module.scss'; @@ -36,6 +37,7 @@ export const PREMIUM_FEATURE_TITLES: Record = { animated_userpics: 'PremiumPreviewAnimatedProfiles', emoji_status: 'PremiumPreviewEmojiStatus', translations: 'PremiumPreviewTranslations', + stories: 'PremiumPreviewStories', }; export const PREMIUM_FEATURE_DESCRIPTIONS: Record = { @@ -52,9 +54,11 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record = { animated_userpics: 'PremiumPreviewAnimatedProfilesDescription', emoji_status: 'PremiumPreviewEmojiStatusDescription', translations: 'PremiumPreviewTranslationsDescription', + stories: 'PremiumPreviewStoriesDescription', }; export const PREMIUM_FEATURE_SECTIONS = [ + 'stories', 'double_limits', 'more_upload', 'faster_download', @@ -267,6 +271,14 @@ const PremiumFeatureModal: FC = ({ ); } + if (section === 'stories') { + return ( +
+ +
+ ); + } + const i = promo.videoSections.indexOf(section); if (i === -1) return undefined; return ( diff --git a/src/components/main/premium/PremiumMainModal.module.scss b/src/components/main/premium/PremiumMainModal.module.scss index fa6aa70ce..1791b6b7d 100644 --- a/src/components/main/premium/PremiumMainModal.module.scss +++ b/src/components/main/premium/PremiumMainModal.module.scss @@ -34,7 +34,7 @@ .main { padding: 1rem 0.5rem; height: 100%; - overflow: scroll; + overflow-y: scroll; display: flex; flex-direction: column; align-items: center; diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx index 46431fdc7..7ded6861b 100644 --- a/src/components/main/premium/PremiumMainModal.tsx +++ b/src/components/main/premium/PremiumMainModal.tsx @@ -57,6 +57,7 @@ const LIMIT_ACCOUNTS = 4; const STATUS_EMOJI_SIZE = 8 * REM; const PREMIUM_FEATURE_COLOR_ICONS: Record = { + stories: PremiumStatus, double_limits: PremiumLimits, infinite_reactions: PremiumReactions, premium_stickers: PremiumStickers, diff --git a/src/components/main/premium/previews/PremiumFeaturePreviewStories.module.scss b/src/components/main/premium/previews/PremiumFeaturePreviewStories.module.scss new file mode 100644 index 000000000..78d3878f8 --- /dev/null +++ b/src/components/main/premium/previews/PremiumFeaturePreviewStories.module.scss @@ -0,0 +1,43 @@ +.root { + display: flex; + flex-direction: column; + padding-top: 1.5rem; + height: 100%; +} + +.header { + align-self: center; + position: relative; +} + +.circle { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.title { + font-size: 1.25rem; + font-weight: 500; + align-self: center; + margin-top: 1rem; + margin-bottom: 1rem; +} + +.features { + min-height: 5rem; + overflow-y: scroll; + border-top: 0.0625rem solid transparent; + transition: 0.2s ease-in-out border-color; +} + +.mobile { + padding: 1rem 1rem 1rem 2rem; + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.scrolled { + border-color: var(--color-borders); +} diff --git a/src/components/main/premium/previews/PremiumFeaturePreviewStories.tsx b/src/components/main/premium/previews/PremiumFeaturePreviewStories.tsx new file mode 100644 index 000000000..9fc5f1e89 --- /dev/null +++ b/src/components/main/premium/previews/PremiumFeaturePreviewStories.tsx @@ -0,0 +1,123 @@ +import React, { memo, useLayoutEffect, useRef } from '../../../../lib/teact/teact'; +import { withGlobal } from '../../../../global'; + +import type { ApiUser } from '../../../../api/types'; + +import { selectUser } from '../../../../global/selectors'; +import { REM } from '../../../common/helpers/mediaDimensions'; +import { DPR } from '../../../../util/windowEnvironment'; +import { drawGradientCircle } from '../../../common/AvatarStoryCircle'; +import buildClassName from '../../../../util/buildClassName'; + +import useLang from '../../../../hooks/useLang'; +import useScrolledState from '../../../../hooks/useScrolledState'; + +import Avatar from '../../../common/Avatar'; +import PremiumFeatureItem from '../PremiumFeatureItem'; + +import styles from './PremiumFeaturePreviewStories.module.scss'; + +type StateProps = { + currentUser: ApiUser; +}; + +const STORY_FEATURE_TITLES = { + stories_order: 'PremiumStoriesPriority', + stories_stealth: 'PremiumStoriesStealth', + stories_views: 'PremiumStoriesViews', + stories_timer: 'lng_premium_stories_subtitle_expiration', + stories_save: 'PremiumStoriesSaveToGallery', + stories_caption: 'lng_premium_stories_subtitle_caption', + stories_link: 'lng_premium_stories_subtitle_links', +}; + +const STORY_FEATURE_DESCRIPTIONS = { + stories_order: 'PremiumStoriesPriorityDescription', + stories_stealth: 'PremiumStoriesStealthDescription', + stories_views: 'PremiumStoriesViewsDescription', + stories_timer: 'PremiumStoriesExpirationDescription', + stories_save: 'PremiumStoriesSaveToGalleryDescription', + stories_caption: 'PremiumStoriesCaptionDescription', + stories_link: 'PremiumStoriesFormattingDescription', +}; + +const STORY_FEATURE_ICONS = { + stories_order: 'story-priority', + stories_stealth: 'eye-closed-outline', + stories_views: 'eye-outline', + stories_timer: 'timer', + stories_save: 'arrow-down-circle', + stories_caption: 'story-caption', + stories_link: 'link-badge', +}; + +const STORY_FEATURE_ORDER = Object.keys(STORY_FEATURE_TITLES) as (keyof typeof STORY_FEATURE_TITLES)[]; + +const CIRCLE_SIZE = 5.25 * DPR * REM; +const CIRCLE_SEGMENTS = 8; +const CIRCLE_READ_SEGMENTS = 0; + +const PremiumFeaturePreviewVideo = ({ + currentUser, +}: StateProps) => { + // eslint-disable-next-line no-null/no-null + const circleRef = useRef(null); + + const lang = useLang(); + + useLayoutEffect(() => { + if (!circleRef.current) { + return; + } + + drawGradientCircle({ + canvas: circleRef.current, + size: CIRCLE_SIZE, + segmentsCount: CIRCLE_SEGMENTS, + color: 'purple', + readSegmentsCount: CIRCLE_READ_SEGMENTS, + readSegmentColor: 'transparent', + }); + }, []); + + const { handleScroll, isAtBeginning } = useScrolledState(); + + const maxSize = CIRCLE_SIZE / DPR; + + return ( +
+
+ + +
+
{lang('UpgradedStories')}
+
+ {STORY_FEATURE_ORDER.map((section, index) => { + return ( + + ); + })} +
{lang('lng_premium_stories_about_mobile')}
+
+
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + return { + currentUser: selectUser(global, global.currentUserId!)!, + }; + }, +)(PremiumFeaturePreviewVideo)); diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 7896d32ce..9b5d4d61d 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -207,7 +207,7 @@ const MediaViewer: FC = ({ const prevMediaId = usePrevious(mediaId); const prevAvatarOwner = usePrevious(avatarOwner); const prevBestImageData = usePrevious(bestImageData); - const textParts = message ? renderMessageText(message) : undefined; + const textParts = message ? renderMessageText({ message, forcePlayback: true }) : undefined; const hasFooter = Boolean(textParts); const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId; diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index 736e82a65..3805b8fb0 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -147,7 +147,7 @@ const MediaViewerContent: FC = (props) => { if (!message) return undefined; const textParts = message.content.action?.type === 'suggestProfilePhoto' ? lang('Conversation.SuggestedPhotoTitle') - : renderMessageText(message); + : renderMessageText({ message, forcePlayback: true }); const hasFooter = Boolean(textParts); const posterSize = message && calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo); diff --git a/src/components/middle/ChatReportPanel.tsx b/src/components/middle/ChatReportPanel.tsx index ae40937e0..4edfd970b 100644 --- a/src/components/middle/ChatReportPanel.tsx +++ b/src/components/middle/ChatReportPanel.tsx @@ -37,7 +37,7 @@ const ChatReportPanel: FC = ({ }) => { const { openAddContactDialog, - blockContact, + blockUser, reportSpam, deleteChat, leaveChannel, @@ -51,7 +51,6 @@ const ChatReportPanel: FC = ({ const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag(); const [shouldReportSpam, setShouldReportSpam] = useState(true); const [shouldDeleteChat, setShouldDeleteChat] = useState(true); - const { accessHash } = chat || {}; const { isAutoArchived, canReportSpam, canAddContact, canBlockContact, } = settings || {}; @@ -66,7 +65,7 @@ const ChatReportPanel: FC = ({ const handleConfirmBlock = useLastCallback(() => { closeBlockUserModal(); - blockContact({ contactId: chatId, accessHash: accessHash! }); + blockUser({ userId: chatId }); if (canReportSpam && shouldReportSpam) { reportSpam({ chatId }); } diff --git a/src/components/middle/EmojiInteractionAnimation.tsx b/src/components/middle/EmojiInteractionAnimation.tsx index 0914bd0ff..7466591db 100644 --- a/src/components/middle/EmojiInteractionAnimation.tsx +++ b/src/components/middle/EmojiInteractionAnimation.tsx @@ -108,7 +108,7 @@ const EmojiInteractionAnimation: FC = ({ tgsUrl={effectTgsUrl} play quality={IS_ANDROID ? 0.5 : undefined} - forceOnHeavyAnimation + forceAlways noLoop onLoad={startPlaying} /> diff --git a/src/components/middle/MessageListBotInfo.tsx b/src/components/middle/MessageListBotInfo.tsx index 9a86baf6c..c44575e99 100644 --- a/src/components/middle/MessageListBotInfo.tsx +++ b/src/components/middle/MessageListBotInfo.tsx @@ -19,7 +19,7 @@ import useMedia from '../../hooks/useMedia'; import useLang from '../../hooks/useLang'; import OptimizedVideo from '../ui/OptimizedVideo'; -import Skeleton from '../ui/Skeleton'; +import Skeleton from '../ui/placeholder/Skeleton'; import styles from './MessageListBotInfo.module.scss'; diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 9c8b71459..272be8857 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -592,6 +592,7 @@ const AttachmentModal: FC = ({ canSendPlainText className="attachment-modal-symbol-menu with-menu-transitions" idPrefix="attachment" + forceDarkTheme={forceDarkTheme} /> ; placeholder: string; + timedPlaceholderLangKey?: string; + timedPlaceholderDate?: number; forcedPlaceholder?: string; noFocusInterception?: boolean; canAutoFocus: boolean; @@ -114,6 +117,8 @@ const MessageInput: FC = ({ isActive, getHtml, placeholder, + timedPlaceholderLangKey, + timedPlaceholderDate, forcedPlaceholder, canSendPlainText, canAutoFocus, @@ -165,6 +170,16 @@ const MessageInput: FC = ({ const { isMobile } = useAppLayout(); const isMobileDevice = isMobile && (IS_IOS || IS_ANDROID); + const [shouldDisplayTimer, setShouldDisplayTimer] = useState(false); + + useEffect(() => { + setShouldDisplayTimer(Boolean(timedPlaceholderLangKey && timedPlaceholderDate)); + }, [timedPlaceholderDate, timedPlaceholderLangKey]); + + const handleTimerEnd = useLastCallback(() => { + setShouldDisplayTimer(false); + }); + useInputCustomEmojis( getHtml, inputRef, @@ -572,7 +587,9 @@ const MessageInput: FC = ({ > {!isAttachmentModalInput && !canSendPlainText && } - {placeholder} + {shouldDisplayTimer ? ( + + ) : placeholder} )} diff --git a/src/components/middle/composer/StickerPicker.tsx b/src/components/middle/composer/StickerPicker.tsx index c0b2534b7..99d51abd1 100644 --- a/src/components/middle/composer/StickerPicker.tsx +++ b/src/components/middle/composer/StickerPicker.tsx @@ -294,6 +294,7 @@ const StickerPicker: FC = ({ noPlay={!canAnimate || !loadAndPlay} observeIntersection={observeIntersectionForCovers} sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined} + forcePlayback /> )} @@ -314,6 +315,7 @@ const StickerPicker: FC = ({ withTranslucentThumb={isTranslucent} onClick={selectStickerSet} clickArg={index} + forcePlayback /> ); } @@ -375,6 +377,7 @@ const StickerPicker: FC = ({ onStickerUnfave={handleStickerUnfave} onStickerFave={handleStickerFave} onStickerRemoveRecent={handleRemoveRecentSticker} + forcePlayback /> ))}
diff --git a/src/components/middle/composer/StickerSetCover.tsx b/src/components/middle/composer/StickerSetCover.tsx index fef4a4732..d6f70ce90 100644 --- a/src/components/middle/composer/StickerSetCover.tsx +++ b/src/components/middle/composer/StickerSetCover.tsx @@ -29,6 +29,7 @@ type OwnProps = { stickerSet: ApiStickerSet; size?: number; noPlay?: boolean; + forcePlayback?: boolean; observeIntersection: ObserveFn; sharedCanvasRef?: React.RefObject; }; @@ -37,6 +38,7 @@ const StickerSetCover: FC = ({ stickerSet, size = STICKER_SIZE_PICKER_HEADER, noPlay, + forcePlayback, observeIntersection, sharedCanvasRef, }) => { @@ -90,6 +92,7 @@ const StickerSetCover: FC = ({ isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet)} sharedCanvas={sharedCanvasRef?.current || undefined} sharedCanvasCoords={coords} + forceAlways={forcePlayback} /> ) : (isVideo && !shouldFallbackToStatic) ? ( = ({ src={mediaData} canPlay={shouldPlay} style={colorFilter} + isPriority={forcePlayback} loop disablePictureInPicture /> diff --git a/src/components/middle/composer/SymbolMenu.tsx b/src/components/middle/composer/SymbolMenu.tsx index 07d66494e..69f8783ae 100644 --- a/src/components/middle/composer/SymbolMenu.tsx +++ b/src/components/middle/composer/SymbolMenu.tsx @@ -2,7 +2,7 @@ import React, { memo, useEffect, useLayoutEffect, useRef, useState, } from '../../../lib/teact/teact'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; -import { getActions, withGlobal } from '../../../global'; +import { withGlobal } from '../../../global'; import type { FC } from '../../../lib/teact/teact'; import type { ApiSticker, ApiVideo } from '../../../api/types'; @@ -10,7 +10,7 @@ import type { GlobalActions } from '../../../global'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; -import { selectTabState, selectIsCurrentUserPremium, selectIsContextMenuTranslucent } from '../../../global/selectors'; +import { selectTabState, selectIsContextMenuTranslucent } from '../../../global/selectors'; import useLastCallback from '../../../hooks/useLastCallback'; import useShowTransition from '../../../hooks/useShowTransition'; @@ -69,7 +69,6 @@ export type OwnProps = { type StateProps = { isLeftColumnShown: boolean; - isCurrentUserPremium?: boolean; isBackgroundTranslucent?: boolean; }; @@ -83,7 +82,6 @@ const SymbolMenu: FC = ({ canSendGifs, isMessageComposer, isLeftColumnShown, - isCurrentUserPremium, idPrefix, isAttachmentModal, canSendPlainText, @@ -105,7 +103,6 @@ const SymbolMenu: FC = ({ addRecentEmoji, addRecentCustomEmoji, }) => { - const { loadPremiumSetStickers } = getActions(); const [activeTab, setActiveTab] = useState(0); const [recentEmojis, setRecentEmojis] = useState([]); const [recentCustomEmojis, setRecentCustomEmojis] = useState([]); @@ -130,12 +127,6 @@ const SymbolMenu: FC = ({ setActiveTab(STICKERS_TAB_INDEX); }, [canSendPlainText]); - useEffect(() => { - if (isCurrentUserPremium) { - loadPremiumSetStickers(); - } - }, [isCurrentUserPremium, loadPremiumSetStickers]); - useLayoutEffect(() => { if (!isMobile || !isOpen || isAttachmentModal) { return undefined; @@ -356,7 +347,6 @@ export default memo(withGlobal( (global): StateProps => { return { isLeftColumnShown: selectTabState(global).isLeftColumnShown, - isCurrentUserPremium: selectIsCurrentUserPremium(global), isBackgroundTranslucent: selectIsContextMenuTranslucent(global), }; }, diff --git a/src/components/middle/composer/SymbolMenuButton.tsx b/src/components/middle/composer/SymbolMenuButton.tsx index cd056dcb6..f5b6c1f29 100644 --- a/src/components/middle/composer/SymbolMenuButton.tsx +++ b/src/components/middle/composer/SymbolMenuButton.tsx @@ -29,6 +29,7 @@ type OwnProps = { canSendStickers?: boolean; isMessageComposer?: boolean; idPrefix: string; + forceDarkTheme?: boolean; openSymbolMenu: VoidFunction; closeSymbolMenu: VoidFunction; onCustomEmojiSelect: (emoji: ApiSticker) => void; @@ -65,6 +66,7 @@ const SymbolMenuButton: FC = ({ canSendPlainText, isSymbolMenuForced, className, + forceDarkTheme, inputCssSelector = EDITABLE_INPUT_CSS_SELECTOR, openSymbolMenu, closeSymbolMenu, @@ -196,7 +198,7 @@ const SymbolMenuButton: FC = ({ addRecentCustomEmoji={addRecentCustomEmoji} isAttachmentModal={isAttachmentModal} canSendPlainText={canSendPlainText} - className={className} + className={buildClassName(className, forceDarkTheme && 'component-theme-dark')} positionX={isAttachmentModal ? positionX : undefined} positionY={isAttachmentModal ? positionY : undefined} transformOriginX={isAttachmentModal ? transformOriginX : undefined} diff --git a/src/components/middle/composer/hooks/useClipboardPaste.ts b/src/components/middle/composer/hooks/useClipboardPaste.ts index 7c2506dc0..919a46e22 100644 --- a/src/components/middle/composer/hooks/useClipboardPaste.ts +++ b/src/components/middle/composer/hooks/useClipboardPaste.ts @@ -5,7 +5,9 @@ import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../ap import { ApiMessageEntityTypes } from '../../../../api/types'; import buildAttachment from '../helpers/buildAttachment'; -import { DEBUG, EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../../config'; +import { + DEBUG, EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID, +} from '../../../../config'; import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems'; import parseMessageInput, { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput'; import cleanDocsHtml from '../../../../lib/cleanDocsHtml'; @@ -86,7 +88,7 @@ const useClipboardPaste = ( } const input = document.activeElement; - if (input && ![EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID].includes(input.id)) { + if (input && ![EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID].includes(input.id)) { return; } diff --git a/src/components/middle/message/AnimatedEmoji.tsx b/src/components/middle/message/AnimatedEmoji.tsx index 63d7f432a..249fca3db 100644 --- a/src/components/middle/message/AnimatedEmoji.tsx +++ b/src/components/middle/message/AnimatedEmoji.tsx @@ -66,7 +66,7 @@ const AnimatedEmoji: FC = ({ noLoad={!isIntersecting} forcePreview={forceLoadPreview} play={isIntersecting} - forceOnHeavyAnimation + forceAlways ref={ref} className={buildClassName('AnimatedEmoji media-inner', sticker?.id === LIKE_STICKER_ID && 'like-sticker-thumb')} style={style} diff --git a/src/components/middle/message/Game.tsx b/src/components/middle/message/Game.tsx index 6e7886942..14bf8c389 100644 --- a/src/components/middle/message/Game.tsx +++ b/src/components/middle/message/Game.tsx @@ -8,7 +8,7 @@ import { getGamePreviewPhotoHash, getGamePreviewVideoHash, getMessageText } from import useMedia from '../../../hooks/useMedia'; -import Skeleton from '../../ui/Skeleton'; +import Skeleton from '../../ui/placeholder/Skeleton'; import './Game.scss'; diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index 80f59db71..b21d7cc49 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -16,7 +16,7 @@ import useLang from '../../../hooks/useLang'; import useMedia from '../../../hooks/useMedia'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; -import Skeleton from '../../ui/Skeleton'; +import Skeleton from '../../ui/placeholder/Skeleton'; import './Invoice.scss'; diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index d34857655..8bafab076 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -15,7 +15,7 @@ import { } from '../../../global/helpers'; import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat'; import { - getMetersPerPixel, getVenueColor, getVenueIconUrl, prepareMapUrl, + getMetersPerPixel, getVenueColor, getVenueIconUrl, } from '../../../util/map'; import { getServerTime } from '../../../util/serverTime'; @@ -29,7 +29,7 @@ import usePrevious from '../../../hooks/usePrevious'; import useInterval from '../../../hooks/useInterval'; import Avatar from '../../common/Avatar'; -import Skeleton from '../../ui/Skeleton'; +import Skeleton from '../../ui/placeholder/Skeleton'; import './Location.scss'; @@ -57,7 +57,7 @@ const Location: FC = ({ message, peer, }) => { - const { openUrl } = getActions(); + const { openMapModal } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null @@ -95,8 +95,7 @@ const Location: FC = ({ }, [type, point, zoom]); const handleClick = () => { - const url = prepareMapUrl(point.lat, point.long, zoom); - openUrl({ url }); + openMapModal({ geoPoint: point, zoom }); }; const updateCountdown = useLastCallback((countdownEl: HTMLDivElement) => { @@ -194,6 +193,7 @@ const Location: FC = ({ className="full-media map" src={mapBlobUrl} alt="Location on a map" + draggable={false} style={`width: ${DEFAULT_MAP_CONFIG.width}px; height: ${DEFAULT_MAP_CONFIG.height}px;`} /> ); @@ -224,14 +224,14 @@ const Location: FC = ({ return (
- +
); } } return ( - + ); } diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index 70f00f8bd..aff1209b7 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -33,7 +33,7 @@ import useAppLayout from '../../../hooks/useAppLayout'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; import MenuSeparator from '../../ui/MenuSeparator'; -import Skeleton from '../../ui/Skeleton'; +import Skeleton from '../../ui/placeholder/Skeleton'; import ReactionSelector from './ReactionSelector'; import AvatarList from '../../common/AvatarList'; diff --git a/src/components/middle/message/ReactionAnimatedEmoji.tsx b/src/components/middle/message/ReactionAnimatedEmoji.tsx index 7ec0bd543..6a6c4295a 100644 --- a/src/components/middle/message/ReactionAnimatedEmoji.tsx +++ b/src/components/middle/message/ReactionAnimatedEmoji.tsx @@ -135,7 +135,7 @@ const ReactionAnimatedEmoji: FC = ({ tgsUrl={mediaDataEffect} play={isIntersecting} noLoop - forceOnHeavyAnimation + forceAlways onEnded={handleEnded} /> {isCustom ? ( @@ -148,7 +148,7 @@ const ReactionAnimatedEmoji: FC = ({ tgsUrl={mediaDataCenterIcon} play={isIntersecting} noLoop - forceOnHeavyAnimation + forceAlways onLoad={markAnimationLoaded} onEnded={unmarkAnimationLoaded} /> diff --git a/src/components/middle/message/ReactionPicker.tsx b/src/components/middle/message/ReactionPicker.tsx index 315e1217c..3c460ecad 100644 --- a/src/components/middle/message/ReactionPicker.tsx +++ b/src/components/middle/message/ReactionPicker.tsx @@ -41,6 +41,7 @@ interface StateProps { isCurrentUserPremium?: boolean; position?: IAnchorPosition; isTranslucent?: boolean; + sendAsMessage?: boolean; } const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 }; @@ -55,9 +56,10 @@ const ReactionPicker: FC = ({ isTranslucent, isCurrentUserPremium, withCustomReactions, + sendAsMessage, }) => { const { - toggleReaction, closeReactionPicker, sendMessage, showNotification, + toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, } = getActions(); const lang = useLang(); @@ -121,33 +123,42 @@ const ReactionPicker: FC = ({ closeReactionPicker(); }); - const handleStoryReactionSelect = useLastCallback((reaction: ApiReaction | ApiSticker) => { + const handleStoryReactionSelect = useLastCallback((item: ApiReaction | ApiSticker) => { + const reaction = 'id' in item ? { documentId: item.id } : item; + + const sticker = 'documentId' in item + ? getGlobal().customEmojis.byId[item.documentId] : 'emoticon' in item ? undefined : item; + + if (sticker && !sticker.isFree && !isCurrentUserPremium) { + showNotification({ + message: lang('UnlockPremiumEmojiHint'), + action: { + action: 'openPremiumModal', + payload: { initialSection: 'animated_emoji' }, + }, + actionText: lang('PremiumMore'), + }); + + closeReactionPicker(); + + return; + } + + if (!sendAsMessage) { + sendStoryReaction({ + userId: renderedStoryUserId!, storyId: renderedStoryId!, reaction, shouldAddToRecent: true, + }); + closeReactionPicker(); + return; + } + let text: string | undefined; let entities: ApiMessageEntity[] | undefined; - if ('emoticon' in reaction) { - text = reaction.emoticon; + if ('emoticon' in item) { + text = item.emoticon; } else { - const sticker = 'documentId' in reaction ? getGlobal().customEmojis.byId[reaction.documentId] : reaction; - if (!sticker) { - return; - } - - if (!sticker.isFree && !isCurrentUserPremium) { - showNotification({ - message: lang('UnlockPremiumEmojiHint'), - action: { - action: 'openPremiumModal', - payload: { initialSection: 'animated_emoji' }, - }, - actionText: lang('PremiumMore'), - }); - - closeReactionPicker(); - - return; - } - const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker)); + const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker!)); text = customEmojiMessage.text; entities = customEmojiMessage.entities; } @@ -212,7 +223,7 @@ const ReactionPicker: FC = ({ export default memo(withGlobal((global): StateProps => { const state = selectTabState(global); const { - chatId, messageId, storyUserId, storyId, position, + chatId, messageId, storyUserId, storyId, position, sendAsMessage, } = state.reactionPicker || {}; const story = storyUserId && storyId ? selectUserStory(global, storyUserId, storyId) as ApiStory | ApiStorySkipped @@ -234,6 +245,7 @@ export default memo(withGlobal((global): StateProps => { : areCustomReactionsAllowed || isPrivateChat, isTranslucent: selectIsContextMenuTranslucent(global), isCurrentUserPremium: selectIsCurrentUserPremium(global), + sendAsMessage, }; })(ReactionPicker)); diff --git a/src/components/middle/message/ReactionSelectorReaction.tsx b/src/components/middle/message/ReactionSelectorReaction.tsx index d5b94c27b..590bdfc0a 100644 --- a/src/components/middle/message/ReactionSelectorReaction.tsx +++ b/src/components/middle/message/ReactionSelectorReaction.tsx @@ -64,6 +64,7 @@ const ReactionSelectorReaction: FC = ({ noLoop size={REACTION_SIZE} onEnded={unmarkIsFirstPlay} + forceAlways /> )} {!isFirstPlay && !noAppearAnimation && ( @@ -75,6 +76,7 @@ const ReactionSelectorReaction: FC = ({ size={REACTION_SIZE} onLoad={markAnimationLoaded} onEnded={deactivate} + forceAlways /> )} diff --git a/src/components/middle/message/helpers/copyOptions.ts b/src/components/middle/message/helpers/copyOptions.ts index 1073206a0..c7f0dbb26 100644 --- a/src/components/middle/message/helpers/copyOptions.ts +++ b/src/components/middle/message/helpers/copyOptions.ts @@ -88,7 +88,7 @@ export function getMessageCopyOptions( document.execCommand('copy'); } else { const clipboardText = renderMessageText( - message, undefined, undefined, undefined, undefined, undefined, true, + { message, shouldRenderAsHtml: true }, ); if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!); } diff --git a/src/components/modals/mapModal/MapModal.async.tsx b/src/components/modals/mapModal/MapModal.async.tsx new file mode 100644 index 000000000..33b2f6977 --- /dev/null +++ b/src/components/modals/mapModal/MapModal.async.tsx @@ -0,0 +1,17 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; +import { Bundles } from '../../../util/moduleLoader'; + +import type { OwnProps } from './MapModal'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const MapModalAsync: FC = (props) => { + const { geoPoint } = props; + const MapModal = useModuleLoader(Bundles.Extra, 'MapModal', !geoPoint); + + // eslint-disable-next-line react/jsx-props-no-spreading + return MapModal ? : undefined; +}; + +export default MapModalAsync; diff --git a/src/components/modals/mapModal/MapModal.module.scss b/src/components/modals/mapModal/MapModal.module.scss new file mode 100644 index 000000000..8815d5e00 --- /dev/null +++ b/src/components/modals/mapModal/MapModal.module.scss @@ -0,0 +1,10 @@ +.root { + display: flex; + flex-direction: column; +} + +.buttons { + display: flex; + flex-direction: column; + gap: 0.5rem; +} diff --git a/src/components/modals/mapModal/MapModal.tsx b/src/components/modals/mapModal/MapModal.tsx new file mode 100644 index 000000000..59f8a9f92 --- /dev/null +++ b/src/components/modals/mapModal/MapModal.tsx @@ -0,0 +1,100 @@ +import React, { memo, useMemo } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import type { ApiGeoPoint } from '../../../api/types'; + +import { IS_IOS, IS_MAC_OS } from '../../../util/windowEnvironment'; +import { prepareMapUrl } from '../../../util/map'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import Modal from '../../ui/Modal'; +import Button from '../../ui/Button'; + +import styles from './MapModal.module.scss'; + +export type OwnProps = { + geoPoint?: ApiGeoPoint; + zoom?: number; +}; + +const OpenMapModal = ({ geoPoint, zoom }: OwnProps) => { + const { closeMapModal } = getActions(); + + const lang = useLang(); + + const isOpen = Boolean(geoPoint); + + const handleClose = useLastCallback(() => { + closeMapModal(); + }); + + const [googleUrl, bingUrl, appleUrl, osmUrl] = useMemo(() => { + if (!geoPoint) { + return []; + } + + const google = prepareMapUrl('google', geoPoint, zoom); + const bing = prepareMapUrl('bing', geoPoint, zoom); + const osm = prepareMapUrl('osm', geoPoint, zoom); + const apple = prepareMapUrl('apple', geoPoint, zoom); + + return [google, bing, apple, osm]; + }, [geoPoint, zoom]); + + const openUrl = useLastCallback((url: string) => { + closeMapModal(); + window.open(url, '_blank', 'noopener'); + }); + + const handleGoogleClick = useLastCallback(() => { + openUrl(googleUrl!); + }); + + const handleBingClick = useLastCallback(() => { + openUrl(bingUrl!); + }); + + const handleAppleClick = useLastCallback(() => { + openUrl(appleUrl!); + }); + + const handleOsmClick = useLastCallback(() => { + openUrl(osmUrl!); + }); + + return ( + +
+ {(IS_IOS || IS_MAC_OS) && ( + + )} + + + +
+
+ +
+
+ ); +}; + +export default memo(OpenMapModal); diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 9665de616..dc828c839 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -16,7 +16,7 @@ import useLang from '../../hooks/useLang'; import useMedia from '../../hooks/useMedia'; import Checkbox from '../ui/Checkbox'; -import Skeleton from '../ui/Skeleton'; +import Skeleton from '../ui/placeholder/Skeleton'; import SafeLink from '../common/SafeLink'; import ListItem from '../ui/ListItem'; diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 193a97a10..5aeeeae72 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -38,6 +38,7 @@ import { selectTabState, selectTheme, selectUser, + selectUserFullInfo, selectUserStories, } from '../../global/selectors'; import { captureEvents, SwipeDirection } from '../../util/captureEvents'; @@ -610,8 +611,9 @@ export default memo(withGlobal( if (isUserId(chatId)) { resolvedUserId = chatId; user = selectUser(global, resolvedUserId); + const userFullInfo = selectUserFullInfo(global, chatId); hasCommonChatsTab = user && !user.isSelf && !isUserBot(user); - hasStoriesTab = user?.isSelf || (user?.hasStories && !user.areStoriesHidden); + hasStoriesTab = user && (user.isSelf || (!user.areStoriesHidden && userFullInfo?.hasPinnedStories)); const userStories = hasStoriesTab ? selectUserStories(global, user!.id) : undefined; storyIds = userStories?.pinnedIds; storyByIds = userStories?.byId; diff --git a/src/components/story/MediaAreaOverlay.tsx b/src/components/story/MediaAreaOverlay.tsx new file mode 100644 index 000000000..f662a1d5e --- /dev/null +++ b/src/components/story/MediaAreaOverlay.tsx @@ -0,0 +1,55 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import type { ApiMediaArea } from '../../api/types'; +import type { IDimensions } from '../../global/types'; + +import buildStyle from '../../util/buildStyle'; +import buildClassName from '../../util/buildClassName'; + +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 ( +
+ {mediaAreas?.map((mediaArea) => ( +
handleMediaAreaClick(mediaArea)} + /> + ))} +
+ ); +}; + +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); diff --git a/src/components/story/StealthModeModal.module.scss b/src/components/story/StealthModeModal.module.scss new file mode 100644 index 000000000..b1c4a7a44 --- /dev/null +++ b/src/components/story/StealthModeModal.module.scss @@ -0,0 +1,53 @@ +.root { + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} + +.close { + position: absolute; + top: 0.5rem; + left: 0.5rem; +} + +.stealthIcon { + width: 5rem; + height: 5rem; + font-size: 3rem; + background-color: var(--color-primary); + border-radius: 50%; + + display: grid; + place-items: center; +} + +.title { + margin-top: 0.75rem; + font-weight: 500; + color: var(--color-text); +} + +.description { + font-size: 0.875rem; + margin-top: 0.5rem; + text-align: center; + color: var(--color-text-secondary); +} + +.listItem { + align-self: stretch; +} + +.icon { + color: var(--color-primary) !important; + margin-right: 1rem !important; +} + +.button { + margin-top: 1rem; +} + +.subtitle { + line-height: 1.25rem !important; +} diff --git a/src/components/story/StealthModeModal.tsx b/src/components/story/StealthModeModal.tsx new file mode 100644 index 000000000..ea5abdff5 --- /dev/null +++ b/src/components/story/StealthModeModal.tsx @@ -0,0 +1,135 @@ +import React, { memo, useEffect, useState } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { ApiStealthMode } from '../../api/types'; + +import { selectIsCurrentUserPremium, selectTabState } from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { getServerTime } from '../../util/serverTime'; + +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Modal from '../ui/Modal'; +import Button from '../ui/Button'; +import ListItem from '../ui/ListItem'; +import TextTimer from '../ui/TextTimer'; + +import styles from './StealthModeModal.module.scss'; + +type StateProps = { + isOpen?: boolean; + stealthMode?: ApiStealthMode; + isCurrentUserPremium?: boolean; +}; + +const StealthModeModal = ({ isOpen, stealthMode, isCurrentUserPremium } : StateProps) => { + const { + toggleStealthModal, + activateStealthMode, + showNotification, + openPremiumModal, + } = getActions(); + const [isOnCooldown, setIsOnCooldown] = useState(false); + + useEffect(() => { + if (!stealthMode) return; + const serverTime = getServerTime(); + if (stealthMode.cooldownUntil && stealthMode.cooldownUntil > serverTime) { + setIsOnCooldown(true); + } + }, [stealthMode, isOpen]); + + const lang = useLang(); + + const handleTimerEnds = useLastCallback(() => { + setIsOnCooldown(false); + }); + + const handleClose = useLastCallback(() => { + toggleStealthModal({ isOpen: false }); + }); + + const handleActivate = useLastCallback(() => { + if (!isCurrentUserPremium) { + openPremiumModal({ initialSection: 'stories' }); + return; + } + + activateStealthMode(); + showNotification({ + title: lang('StealthModeOn'), + message: lang('StealthModeOnHint'), + }); + toggleStealthModal({ isOpen: false }); + }); + + return ( + + +
+ +
+
{lang('StealthMode')}
+
+ {lang(isCurrentUserPremium ? 'StealthModeHint' : 'StealthModePremiumHint')} +
+ } + > + {lang('HideRecentViews')} + {lang('HideRecentViewsDescription')} + + } + > + {lang('HideNextViews')} + {lang('HideNextViewsDescription')} + + +
+ ); +}; + +export default memo(withGlobal((global): StateProps => { + const tabState = selectTabState(global); + + return { + isOpen: tabState.storyViewer?.isStealthModalOpen, + stealthMode: global.stories.stealthMode, + isCurrentUserPremium: selectIsCurrentUserPremium(global), + }; +})(StealthModeModal)); diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 1609baeb8..0ca09fc29 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -4,20 +4,24 @@ import React, { import { getActions, getGlobal, withGlobal } from '../../global'; import type { FC } from '../../lib/teact/teact'; -import type { ApiStory, ApiTypeStory, ApiUser } from '../../api/types'; +import type { + ApiStealthMode, ApiStory, ApiTypeStory, ApiUser, +} 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 buildClassName from '../../util/buildClassName'; import renderText from '../common/helpers/renderText'; +import { formatMediaDuration, formatRelativeTime } from '../../util/dateFormat'; import { getUserFirstOrLastName } from '../../global/helpers'; -import { formatRelativeTime } from '../../util/dateFormat'; import { getServerTime } from '../../util/serverTime'; import { selectChat, selectTabState, selectUserStory, selectUserStories, selectIsCurrentUserPremium, } from '../../global/selectors'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; +import download from '../../util/download'; import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout'; import useLang from '../../hooks/useLang'; @@ -33,7 +37,7 @@ import useLongPress from '../../hooks/useLongPress'; import useUnsupportedMedia from '../../hooks/media/useUnsupportedMedia'; import useCanvasBlur from '../../hooks/useCanvasBlur'; import useMediaTransition from '../../hooks/useMediaTransition'; -import { useStoryProps } from './hooks/useStoryProps'; +import useStoryProps from './hooks/useStoryProps'; import Button from '../ui/Button'; import Avatar from '../common/Avatar'; @@ -42,9 +46,10 @@ import StoryProgress from './StoryProgress'; import Composer from '../common/Composer'; import MenuItem from '../ui/MenuItem'; import DropdownMenu from '../ui/DropdownMenu'; -import Skeleton from '../ui/Skeleton'; +import Skeleton from '../ui/placeholder/Skeleton'; import StoryCaption from './StoryCaption'; import AvatarList from '../common/AvatarList'; +import MediaAreaOverlay from './MediaAreaOverlay'; import styles from './StoryViewer.module.scss'; @@ -77,6 +82,7 @@ interface StateProps { isChatExist?: boolean; areChatSettingsLoaded?: boolean; isCurrentUserPremium?: boolean; + stealthMode: ApiStealthMode; } const VIDEO_MIN_READY_STATE = 4; @@ -85,6 +91,8 @@ const SPACEBAR_CODE = 32; const PRIMARY_VIDEO_MIME = 'video/mp4; codecs=hvc1.1.6.L63.00'; const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E'; +const STEALTH_MODE_NOTIFICATION_DURATION = 4000; + function Story({ isSelf, userId, @@ -104,6 +112,7 @@ function Story({ areChatSettingsLoaded, getIsAnimating, isCurrentUserPremium, + stealthMode, onDelete, onClose, onReport, @@ -115,7 +124,7 @@ function Story({ openNextStory, loadUserSkippedStories, openForwardMenu, - openStorySeenBy, + openStoryViewModal, copyStoryLink, toggleStoryPinned, openChat, @@ -123,7 +132,8 @@ function Story({ openStoryPrivacyEditor, loadChatSettings, fetchChat, - loadStorySeenBy, + loadStoryViews, + toggleStealthModal, } = getActions(); const serverTime = getServerTime(); @@ -137,6 +147,7 @@ function Story({ const [isCaptionExpanded, expandCaption, foldCaption] = useFlag(false); const [isPausedBySpacebar, setIsPausedBySpacebar] = useState(false); const [isPausedByLongPress, markIsPausedByLongPress, unmarkIsPausedByLongPress] = useFlag(false); + const [isDropdownMenuOpen, markDropdownMenuOpen, unmarkDropdownMenuOpen] = useFlag(false); // eslint-disable-next-line no-null/no-null const videoRef = useRef(null); const { @@ -151,10 +162,15 @@ function Story({ altMediaData, hasFullData, hasThumb, - } = useStoryProps(story); + mediaAreas, + canDownload, + downloadMediaData, + } = useStoryProps(story, isCurrentUserPremium, isDropdownMenuOpen); const isLoadedStory = story && 'content' in story; + const isChangelog = userId === storyChangelogUserId; + const canPinToProfile = useCurrentOrPrev( isSelf && isLoadedStory ? !story.isPinned : undefined, true, @@ -164,12 +180,12 @@ function Story({ true, ); const areViewsExpired = Boolean( - isSelf && !isCurrentUserPremium && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(), + isSelf && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(), ); const canCopyLink = Boolean( isLoadedStory && story.isPublic - && userId !== storyChangelogUserId + && !isChangelog && user?.usernames?.length, ); @@ -177,17 +193,24 @@ function Story({ isLoadedStory && story.isPublic && !story.noForwards - && userId !== storyChangelogUserId + && !isChangelog && !isCaptionExpanded, ); + const canShareOwn = Boolean( + isSelf + && isLoadedStory + && story.isPublic + && !story.noForwards, + ); const canPlayStory = Boolean( hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded && !isPausedBySpacebar && !isPausedByLongPress, ); + const { shouldRender: shouldRenderSkeleton, transitionClassNames: skeletonTransitionClassNames, - } = useShowTransition((isVideo && !hasFullData) || (!isVideo && !previewBlobUrl)); + } = useShowTransition(!hasFullData); const { transitionClassNames: mediaTransitionClassNames, @@ -199,7 +222,7 @@ function Story({ const { shouldRender: shouldRenderComposer, transitionClassNames: composerAppearanceAnimationClassNames, - } = useShowTransition(!isSelf); + } = useShowTransition(!isSelf && !isChangelog); const { shouldRender: shouldRenderCaptionBackdrop, @@ -255,6 +278,16 @@ function Story({ unmarkIsPausedByLongPress(); }); + const handleDropdownMenuOpen = useLastCallback(() => { + markDropdownMenuOpen(); + handlePauseStory(); + }); + + const handleDropdownMenuClose = useLastCallback(() => { + unmarkDropdownMenuOpen(); + handlePlayStory(); + }); + const { onMouseDown: handleLongPressMouseDown, onMouseUp: handleLongPressMouseUp, @@ -279,7 +312,7 @@ function Story({ if (!isSelf || isDeletedStory || areViewsExpired) return; // Refresh recent viewers list each time - loadStorySeenBy({ storyId }); + loadStoryViews({ storyId, isPreload: true }); }, [isDeletedStory, areViewsExpired, isSelf, storyId]); useEffect(() => { @@ -394,8 +427,8 @@ function Story({ handlePauseStory(); }); - const handleOpenStorySeenBy = useLastCallback(() => { - openStorySeenBy({ storyId }); + const handleOpenStoryViewModal = useLastCallback(() => { + openStoryViewModal({ storyId }); }); const handleInfoPrivacyEdit = useLastCallback(() => { @@ -437,6 +470,25 @@ function Story({ setStoryViewerMuted({ isMuted: !isMuted }); }); + const handleOpenStealthModal = useLastCallback(() => { + if (stealthMode.activeUntil && getServerTime() < stealthMode.activeUntil) { + const diff = stealthMode.activeUntil - getServerTime(); + showNotification({ + title: lang('StealthModeOn'), + message: lang('Story.ToastStealthModeActiveText', formatMediaDuration(diff)), + duration: STEALTH_MODE_NOTIFICATION_DURATION, + }); + return; + } + + toggleStealthModal({ isOpen: true }); + }); + + const handleDownload = useLastCallback(() => { + if (!downloadMediaData) return; + download(downloadMediaData, `story-${userId}-${storyId}.${isVideo ? 'mp4' : 'jpg'}`); + }); + useEffect(() => { if (!isDeletedStory) return; @@ -457,7 +509,7 @@ function Story({ onClick={onTrigger} ariaLabel={lang('AccDescrOpenMenu2')} > - + ); }; @@ -571,6 +623,7 @@ function Story({ className={buildClassName( 'icon', isMuted || noSound ? 'icon-speaker-muted-story' : 'icon-speaker-story', + styles.topIcon, )} aria-hidden /> @@ -580,8 +633,8 @@ function Story({ className={buildClassName(styles.button, styles.buttonMenu)} trigger={MenuButton} positionX="right" - onOpen={handlePauseStory} - onClose={handlePlayStory} + onOpen={handleDropdownMenuOpen} + onClose={handleDropdownMenuClose} > {canCopyLink && {lang('CopyLink')}} {canPinToProfile && ( @@ -590,8 +643,14 @@ function Story({ {canUnpinFromProfile && ( {lang('ArchiveStory')} )} + {canDownload && ( + + {lang('lng_media_download')} + + )} + {lang('StealthMode')} + {!isSelf && {lang('lng_report_story')}} {isSelf && {lang('Delete')}} - {!isSelf && {lang('Report')}}
@@ -608,7 +667,7 @@ function Story({ }, [story]); function renderRecentViewers() { - const { viewsCount } = story as ApiStory; + const { viewsCount, reactionsCount } = story as ApiStory; if (!viewsCount) { return ( @@ -625,16 +684,23 @@ function Story({ styles.recentViewersInteractive, appearanceAnimationClassNames, )} - onClick={handleOpenStorySeenBy} + onClick={handleOpenStoryViewModal} > {!areViewsExpired && Boolean(recentViewers?.length) && ( )} - {lang('Views', viewsCount, 'i')} + {lang('Views', viewsCount, 'i')} + {Boolean(reactionsCount) && ( + + + {reactionsCount} + + )} ); } @@ -662,11 +728,7 @@ function Story({ )} {shouldRenderSkeleton && ( - + )} {!isVideo && fullMediaData && ( )} + {isSelf && renderRecentViewers()} + {canShareOwn && ( + + )} {shouldRenderCaptionBackdrop && (
)} + {hasText &&
} {hasText && ( )} @@ -743,8 +820,8 @@ function Story({ isReady={!isSelf} messageListType="thread" isMobile={getIsMobile()} - editableInputCssSelector="#editable-story-input-text" - editableInputId="editable-story-input-text" + editableInputCssSelector={EDITABLE_STORY_INPUT_CSS_SELECTOR} + editableInputId={EDITABLE_STORY_INPUT_ID} inputId="story-input-text" className={buildClassName(styles.composer, composerAppearanceAnimationClassNames)} inputPlaceholder={lang('ReplyPrivately')} @@ -765,16 +842,23 @@ export default memo(withGlobal((global, { const chat = selectChat(global, userId); const tabState = selectTabState(global); const { - storyViewer: { isMuted, storyIdSeenBy, isPrivacyModalOpen }, + storyViewer: { + isMuted, + viewModal, + isPrivacyModalOpen, + isStealthModalOpen, + }, forwardMessages: { storyId: forwardedStoryId }, premiumModal, + safeLinkModalUrl, + mapModal, } = tabState; const { isOpen: isPremiumModalOpen } = premiumModal || {}; const { orderedIds, pinnedIds, archiveIds } = selectUserStories(global, userId) || {}; const story = selectUserStory(global, userId, storyId); const shouldForcePause = Boolean( - storyIdSeenBy || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen - || isPremiumModalOpen || isDeleteModalOpen, + viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen + || isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal, ); return { @@ -789,5 +873,6 @@ export default memo(withGlobal((global, { viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, isChatExist: Boolean(chat), areChatSettingsLoaded: Boolean(chat?.settings), + stealthMode: global.stories.stealthMode, }; })(Story)); diff --git a/src/components/story/StoryCaption.tsx b/src/components/story/StoryCaption.tsx index 6aed59291..631f99cae 100644 --- a/src/components/story/StoryCaption.tsx +++ b/src/components/story/StoryCaption.tsx @@ -1,15 +1,17 @@ import React, { memo, useEffect, useRef, useState, } from '../../lib/teact/teact'; +import { requestMutation } from '../../lib/fasterdom/fasterdom'; +import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom'; import type { ApiStory } from '../../api/types'; import buildClassName from '../../util/buildClassName'; -import { requestMutation } from '../../lib/fasterdom/fasterdom'; -import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom'; +import { REM } from '../common/helpers/mediaDimensions'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import useLang from '../../hooks/useLang'; +import useShowTransition from '../../hooks/useShowTransition'; import MessageText from '../common/MessageText'; @@ -18,34 +20,39 @@ import styles from './StoryViewer.module.scss'; interface OwnProps { story: ApiStory; isExpanded: boolean; - onExpand: NoneToVoidFunction; className?: string; + onExpand: NoneToVoidFunction; + onFold?: NoneToVoidFunction; } const EXPAND_ANIMATION_DURATION_MS = 400; -const OVERFLOW_THRESHOLD_PX = 4; +const OVERFLOW_THRESHOLD_PX = 5.75 * REM; function StoryCaption({ - story, isExpanded, className, onExpand, + story, isExpanded, className, onExpand, onFold, }: OwnProps) { const lang = useLang(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const contentRef = useRef(null); - const [hasOverflow, setHasOverflow] = useState(false); - const [height, setHeight] = useState(0); + // eslint-disable-next-line no-null/no-null + const showMoreButtonRef = useRef(null); + + const caption = story.content.text; + + const [hasOverflow, setHasOverflow] = useState(false); const prevIsExpanded = usePrevDuringAnimation(isExpanded || undefined, EXPAND_ANIMATION_DURATION_MS); + const isInExpandedState = isExpanded || prevIsExpanded; useEffect(() => { if (!ref.current) { return; } - const { scrollHeight, clientHeight } = ref.current; - setHasOverflow(scrollHeight - clientHeight > OVERFLOW_THRESHOLD_PX); - setHeight(scrollHeight - clientHeight); - }, []); + const { clientHeight } = ref.current; + setHasOverflow(clientHeight > OVERFLOW_THRESHOLD_PX); + }, [caption]); useEffect(() => { requestMutation(() => { @@ -59,14 +66,38 @@ function StoryCaption({ removeExtraClass(contentRef.current, styles.animate); } }); - }, [height, isExpanded]); + }, [isExpanded]); + + const canExpand = hasOverflow && !isInExpandedState; + const { shouldRender: shouldRenderShowMore, transitionClassNames } = useShowTransition( + canExpand, undefined, true, 'slow', true, + ); + + useEffect(() => { + if (!showMoreButtonRef.current || !contentRef.current) { + return; + } + + const button = showMoreButtonRef.current; + const container = contentRef.current; + + const { offsetWidth } = button; + requestMutation(() => { + container.style.setProperty('--expand-button-width', `${offsetWidth}px`); + }); + }, [canExpand]); + + useEffect(() => { + if (!isExpanded) { + ref.current?.scrollTo({ top: 0 }); + } + }, [isExpanded]); - const canExpand = hasOverflow && !isExpanded; const fullClassName = buildClassName( styles.captionContent, hasOverflow && !isExpanded && styles.hasOverflow, - (isExpanded || prevIsExpanded) && styles.expanded, - canExpand && styles.captionInteractive, + isInExpandedState && styles.expanded, + shouldRenderShowMore && styles.withShowMore, ); return ( @@ -75,22 +106,28 @@ function StoryCaption({ ref={contentRef} className={fullClassName} role={canExpand ? 'button' : undefined} - style={`--scroll-height: ${isExpanded ? height : 0}px;`} - onClick={canExpand ? () => onExpand() : undefined} + onClick={canExpand ? onExpand : onFold} > -
- {hasOverflow && ( -
- {lang('Story.CaptionShowMore')} -
- )} - +
+ {shouldRenderShowMore && ( +
+ {lang('Story.CaptionShowMore')} +
+ )}
); } diff --git a/src/components/story/StorySettings.module.scss b/src/components/story/StorySettings.module.scss index 9ce6b9e79..e1eb08681 100644 --- a/src/components/story/StorySettings.module.scss +++ b/src/components/story/StorySettings.module.scss @@ -64,7 +64,6 @@ .option { display: flex; align-items: center; - width: 100%; position: relative; overflow: hidden; margin-bottom: 0; @@ -147,11 +146,16 @@ } .action { + width: 100%; color: #8774E1; cursor: var(--custom-cursor, pointer); opacity: 0.8; transition: opacity 200ms; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + > :global(.icon) { font-size: 0.875rem; line-height: 1; @@ -170,6 +174,7 @@ display: flex; flex-direction: column; align-items: flex-start; + overflow: hidden; } .footer { diff --git a/src/components/story/StorySettings.tsx b/src/components/story/StorySettings.tsx index 49260e7f0..03c1a2580 100644 --- a/src/components/story/StorySettings.tsx +++ b/src/components/story/StorySettings.tsx @@ -38,7 +38,7 @@ interface StateProps { currentUserId: string; } -type PrivacyAction = 'blockUserIds' | 'closeFriends' | 'allowUserIds'; +type PrivacyAction = 'blockUserIds' | 'closeFriends' | 'blockContactUserIds' | 'allowUserIds'; interface PrivacyOption { name: string; @@ -53,13 +53,13 @@ const OPTIONS: PrivacyOption[] = [{ value: 'everybody', color: ['#50ABFF', '#007AFF'], icon: 'channel-filled', - actions: undefined, + actions: 'blockUserIds', }, { name: 'StoryPrivacyOptionContacts', value: 'contacts', color: ['#C36EFF', '#8B60FA'], icon: 'user-filled', - actions: 'blockUserIds', + actions: 'blockContactUserIds', }, { name: 'StoryPrivacyOptionCloseFriends', value: 'closeFriends', @@ -82,7 +82,13 @@ enum Screens { } function StorySettings({ - isOpen, story, visibility, contactListIds, usersById, currentUserId, onClose, + isOpen, + story, + visibility, + contactListIds, + usersById, + currentUserId, + onClose, }: OwnProps & StateProps) { const { editStoryPrivacy, toggleStoryPinned } = getActions(); @@ -91,6 +97,7 @@ function StorySettings({ const [privacy, setPrivacy] = useState(visibility); const [isPinned, setIsPinned] = useState(story?.isPinned); const [activeKey, setActiveKey] = useState(Screens.privacy); + const [editingBlockingCategory, setEditingBlockingCategory] = useState('everybody'); const isBackButton = activeKey !== Screens.privacy; const closeFriendIds = useMemo(() => { @@ -107,6 +114,11 @@ function StorySettings({ return undefined; }, [activeKey, currentUserId, privacy?.allowUserIds]); + const selectedBlockedIds = useMemo(() => { + if (editingBlockingCategory !== privacy?.visibility) return []; + return privacy?.blockUserIds || []; + }, [editingBlockingCategory, privacy?.blockUserIds, privacy?.visibility]); + const handleAllowUserIdsChange = useLastCallback((newIds: string[]) => { setPrivacy({ ...privacy!, @@ -118,6 +130,7 @@ function StorySettings({ setPrivacy({ ...privacy!, blockUserIds: newIds, + visibility: editingBlockingCategory, }); }); @@ -160,6 +173,12 @@ function StorySettings({ break; case 'blockUserIds': setActiveKey(Screens.denyList); + setEditingBlockingCategory('everybody'); + break; + case 'blockContactUserIds': + setActiveKey(Screens.denyList); + setEditingBlockingCategory('contacts'); + break; } } @@ -193,13 +212,14 @@ function StorySettings({ return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i'); } - if (action === 'blockUserIds') { - if (!privacy?.blockUserIds || privacy.blockUserIds.length === 0) { + if ((action === 'blockUserIds' && privacy?.visibility === 'everybody') + || (action === 'blockContactUserIds' && privacy?.visibility === 'contacts')) { + if (!privacy?.blockUserIds?.length) { return lang('StoryPrivacyOptionContactsDetail'); } if (privacy.blockUserIds.length === 1) { - return lang('StoryPrivacyOptionExcludePerson', getUserFullName(usersById[closeFriendIds[0]])); + return lang('StoryPrivacyOptionExcludePerson', getUserFullName(usersById[privacy.blockUserIds[0]])); } return lang('StoryPrivacyOptionExcludePeople', privacy.blockUserIds.length, 'i'); @@ -254,7 +274,7 @@ function StorySettings({ contactListIds={contactListIds} currentUserId={currentUserId} usersById={usersById} - selectedIds={privacy?.blockUserIds} + selectedIds={selectedBlockedIds} onSelect={handleDenyUserIdsChange} /> ); @@ -309,7 +329,7 @@ function StorySettings({ tabIndex={0} role="button" className={styles.action} - aria-label={lang('Change List')} + aria-label={lang('Edit')} onClick={(e) => { handleActionClick(e, option.actions!); }} > {renderActionName(option.actions)} diff --git a/src/components/story/StorySlides.tsx b/src/components/story/StorySlides.tsx index e7830a5db..a4eca56fd 100644 --- a/src/components/story/StorySlides.tsx +++ b/src/components/story/StorySlides.tsx @@ -16,7 +16,8 @@ import useLastCallback from '../../hooks/useLastCallback'; import usePrevious from '../../hooks/usePrevious'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useSignal from '../../hooks/useSignal'; -import { useSlideSizes } from './hooks/useSlideSizes'; +import useSlideSizes from './hooks/useSlideSizes'; +import useHistoryBack from '../../hooks/useHistoryBack'; import Story from './Story'; import StoryPreview from './StoryPreview'; @@ -24,6 +25,7 @@ import StoryPreview from './StoryPreview'; import styles from './StoryViewer.module.scss'; interface OwnProps { + isOpen?: boolean; isReportModalOpen?: boolean; isDeleteModalOpen?: boolean; onDelete: (storyId: number) => void; @@ -49,8 +51,20 @@ const ANIMATION_TO_ACTIVE_SCALE = '3'; const ANIMATION_FROM_ACTIVE_SCALE = `${FROM_ACTIVE_SCALE_VALUE}`; function StorySlides({ - userIds, currentUserId, currentStoryId, isSingleUser, isSingleStory, isPrivate, isArchive, byUserId, - isReportModalOpen, isDeleteModalOpen, onDelete, onClose, onReport, + userIds, + currentUserId, + currentStoryId, + isOpen, + isSingleUser, + isSingleStory, + isPrivate, + isArchive, + byUserId, + isReportModalOpen, + isDeleteModalOpen, + onDelete, + onClose, + onReport, }: OwnProps & StateProps) { const [renderingUserId, setRenderingUserId] = useState(currentUserId); const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId); @@ -64,6 +78,12 @@ function StorySlides({ const rendersRef = useRef>({}); const [getIsAnimating, setIsAnimating] = useSignal(false); + useHistoryBack({ + isActive: isOpen, + onBack: onClose, + shouldBeReplaced: true, + }); + function setRef(ref: HTMLDivElement | null, userId: string) { if (!ref) { return; diff --git a/src/components/story/StoryView.tsx b/src/components/story/StoryView.tsx new file mode 100644 index 000000000..4121c2e63 --- /dev/null +++ b/src/components/story/StoryView.tsx @@ -0,0 +1,152 @@ +import React, { memo, useMemo } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { ApiAvailableReaction, ApiStoryView, ApiUser } from '../../api/types'; + +import { selectUser } from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { REM } from '../common/helpers/mediaDimensions'; +import { formatDateAtTime } from '../../util/dateFormat'; +import { getUserFullName } from '../../global/helpers'; + +import useLastCallback from '../../hooks/useLastCallback'; +import useLang from '../../hooks/useLang'; + +import ListItem from '../ui/ListItem'; +import ReactionStaticEmoji from '../common/ReactionStaticEmoji'; +import PrivateChatInfo from '../common/PrivateChatInfo'; + +import styles from './StoryViewModal.module.scss'; + +type OwnProps = { + storyView: ApiStoryView; +}; + +type StateProps = { + user?: ApiUser; + availableReactions?: ApiAvailableReaction[]; +}; + +const CLOSE_ANIMATION_DURATION = 100; +const DEFAULT_REACTION_SIZE = 1.5 * REM; + +const StoryView = ({ + storyView, + user, + availableReactions, +}: OwnProps & StateProps) => { + const { + openChat, closeStoryViewer, unblockUser, blockUser, deleteContact, updateStoryView, + } = getActions(); + + const lang = useLang(); + + const handleClick = useLastCallback(() => { + closeStoryViewer(); + + setTimeout(() => { + openChat({ id: storyView.userId }); + }, CLOSE_ANIMATION_DURATION); + }); + + const contextActions = useMemo(() => { + const { userId, areStoriesBlocked, isUserBlocked } = storyView; + const { isContact } = user || {}; + const fullName = getUserFullName(user); + + const actions = []; + + if (!isUserBlocked) { + if (!areStoriesBlocked) { + actions.push({ + handler: () => { + blockUser({ userId, isOnlyStories: true }); + updateStoryView({ userId, areStoriesBlocked: true }); + }, + title: lang('StoryHideFrom', fullName), + icon: 'hand-stop', + }); + } else { + actions.push({ + handler: () => { + unblockUser({ userId, isOnlyStories: true }); + updateStoryView({ userId, areStoriesBlocked: false }); + }, + title: lang('StoryShowBackTo', fullName), + icon: 'play-story', + }); + } + } + + if (isContact) { + actions.push({ + handler: () => { + deleteContact({ userId }); + }, + title: lang('DeleteContact'), + icon: 'delete-user', + destructive: true, + }); + } else { + actions.push({ + handler: () => { + if (isUserBlocked) { + unblockUser({ userId }); + updateStoryView({ userId, isUserBlocked: false }); + } else { + blockUser({ userId }); + updateStoryView({ userId, isUserBlocked: true }); + } + }, + title: lang(isUserBlocked ? 'Unblock' : 'BlockUser'), + icon: isUserBlocked ? 'user' : 'delete-user', + destructive: !isUserBlocked, + }); + } + + return actions; + }, [lang, storyView, user]); + + return ( + handleClick()} + rightElement={storyView.reaction ? ( + + ) : undefined} + contextActions={contextActions} + withPortalForMenu + menuBubbleClassName={styles.menuBubble} + > + + + ); +}; + +export default memo(withGlobal((global, { storyView }) => { + const user = selectUser(global, storyView.userId); + + return { + user, + availableReactions: global.availableReactions, + }; +})(StoryView)); diff --git a/src/components/story/StoryViewModal.module.scss b/src/components/story/StoryViewModal.module.scss new file mode 100644 index 000000000..13c03dab4 --- /dev/null +++ b/src/components/story/StoryViewModal.module.scss @@ -0,0 +1,125 @@ +.views-list { + display: flex; + flex-direction: column; + padding-bottom: 0 !important; + + height: 35rem; + @supports (height: min(80vh, 35rem)) { + height: min(80vh, 35rem); + } +} + +.views-list-loading { + justify-content: center; +} + +.info { + color: var(--color-text-secondary); + text-align: center; +} + +.centeredInfo { + width: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.view-reaction { + --custom-emoji-size: 1.5rem; + margin-inline-start: 0.25rem; +} + +.header { + display: flex; + flex-direction: column; +} + +.content { + min-height: 17rem; + overflow-y: scroll; + position: relative; + flex-grow: 1; +} + +.top-button { + height: 2rem !important; + line-height: 1; +} + +.contact-filter { + display: flex; + gap: 0.5rem; +} + +.selected { + pointer-events: none; + color: #FFFFFF !important; + background-color: var(--color-interactive-element-hover) !important; +} + +.sort { + position: absolute; + top: 1rem; + right: 1rem; +} + +.sort-button { + padding-inline: 0.5rem !important; + border-radius: 1rem !important; +} + +.icon-sort { + font-size: 1.25rem; + color: #FFFFFF; + margin-inline-end: 0.25rem; +} + +.icon-down { + font-size: 1rem; + color: #FFFFFF; +} + +.search { + margin-block: 0.5rem; +} + +.bottom-info { + font-size: 0.875rem; + margin: 0.25rem 0 0.5rem; +} + +.scrolled { + border-bottom: 0.0625rem solid var(--color-borders); +} + +.footer { + border-top: 0.0625rem solid var(--color-borders); +} + +.close { + margin-block: 0.25rem; +} + +.opacity-fade-in { + animation: fadeIn 0.2s ease-in; +} + +.blocked { + opacity: 0.5; +} + +.check { + margin: 0 0 0 auto !important; +} + +.menuBubble { + max-width: min(25rem, 80vw); +} + +@keyframes fadeIn { + from { + opacity: 0; + } +} diff --git a/src/components/story/StoryViewModal.tsx b/src/components/story/StoryViewModal.tsx new file mode 100644 index 000000000..1e29c8709 --- /dev/null +++ b/src/components/story/StoryViewModal.tsx @@ -0,0 +1,293 @@ +import React, { + memo, useEffect, useMemo, useState, +} from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { FC } from '../../lib/teact/teact'; +import type { ApiStory, ApiStoryView } from '../../api/types'; + +import { + STORY_MIN_REACTIONS_SORT, + STORY_VIEWS_MIN_CONTACTS_FILTER, + STORY_VIEWS_MIN_SEARCH, +} from '../../config'; +import { + selectIsCurrentUserPremium, + selectTabState, + selectUserStory, +} from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { getServerTime } from '../../util/serverTime'; +import renderText from '../common/helpers/renderText'; + +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import useFlag from '../../hooks/useFlag'; +import useScrolledState from '../../hooks/useScrolledState'; +import useDebouncedCallback from '../../hooks/useDebouncedCallback'; + +import Modal from '../ui/Modal'; +import ListItem from '../ui/ListItem'; +import Button from '../ui/Button'; +import InfiniteScroll from '../ui/InfiniteScroll'; +import SearchInput from '../ui/SearchInput'; +import DropdownMenu from '../ui/DropdownMenu'; +import MenuItem from '../ui/MenuItem'; +import PlaceholderChatInfo from '../ui/placeholder/PlaceholderChatInfo'; +import StoryView from './StoryView'; + +import styles from './StoryViewModal.module.scss'; + +interface StateProps { + story?: ApiStory; + isLoading?: boolean; + viewsById?: Record; + nextOffset?: string; + viewersExpirePeriod: number; + isCurrentUserPremium?: boolean; +} + +const REFETCH_DEBOUNCE = 250; + +function StoryViewModal({ + story, + viewersExpirePeriod, + viewsById, + nextOffset, + isLoading, + isCurrentUserPremium, +}: StateProps) { + const { + loadStoryViews, closeStoryViewModal, clearStoryViews, + } = getActions(); + + const [areJustContacts, markJustContacts, unmarkJustContacts] = useFlag(false); + const [areReactionsFirst, markReactionsFirst, unmarkReactionsFirst] = useFlag(true); + const [query, setQuery] = useState(''); + + const lang = useLang(); + + const isOpen = Boolean(story); + const isExpired = Boolean(story?.date) && (story!.date + viewersExpirePeriod) < getServerTime(); + const viewsCount = story?.viewsCount || 0; + const reactionsCount = story?.reactionsCount || 0; + + const shouldShowJustContacts = story?.isPublic && viewsCount > STORY_VIEWS_MIN_CONTACTS_FILTER; + const shouldShowSortByReactions = reactionsCount > STORY_MIN_REACTIONS_SORT; + const shouldShowSearch = viewsCount > STORY_VIEWS_MIN_SEARCH; + const hasHeader = shouldShowJustContacts || shouldShowSortByReactions || shouldShowSearch; + + useEffect(() => { + if (!isOpen) { + setQuery(''); + unmarkJustContacts(); + markReactionsFirst(); + } + }, [isOpen]); + + const refetchViews = useDebouncedCallback(() => { + clearStoryViews({ isLoading: true }); + }, [], REFETCH_DEBOUNCE, true); + + useEffect(() => { + refetchViews(); + }, [areJustContacts, areReactionsFirst, query, refetchViews]); + + const sortedViewIds = useMemo(() => { + if (!viewsById) { + return undefined; + } + + return Object.values(viewsById) + .sort(prepareComparator(areReactionsFirst)) + .map((view) => view.userId); + }, [areReactionsFirst, viewsById]); + + const placeholderCount = !sortedViewIds?.length ? Math.min(viewsCount, 8) : 1; + + const notAllAvailable = Boolean(sortedViewIds?.length) && sortedViewIds!.length < viewsCount && isExpired; + + const handleLoadMore = useLastCallback(() => { + if (!story?.id || nextOffset === undefined) return; + loadStoryViews({ + storyId: story.id, + offset: nextOffset, + areReactionsFirst: areReactionsFirst || undefined, + areJustContacts: areJustContacts || undefined, + query, + }); + }); + + const { handleScroll, isAtBeginning } = useScrolledState(); + + const handleClose = useLastCallback(() => { + closeStoryViewModal(); + }); + + const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { + return ({ onTrigger, isOpen: isMenuOpen }) => ( + + ); + }, [areReactionsFirst, lang]); + + return ( + + {hasHeader && ( +
+ {shouldShowJustContacts && ( +
+ + +
+ )} + {shouldShowSortByReactions && ( + + + {lang('SortByReactions')} + {areReactionsFirst && ( + + )} + + + {lang('SortByTime')} + {!areReactionsFirst && ( + + )} + + + )} + {shouldShowSearch && ( + + )} +
+ )} +
+ {isExpired && !isLoading && !query && Boolean(!sortedViewIds?.length) && ( +
+ {renderText( + lang(isCurrentUserPremium ? 'ServerErrorViewers' : 'ExpiredViewsStub'), + ['simple_markdown', 'emoji'], + )} +
+ )} + {!isLoading && Boolean(query.length) && !sortedViewIds?.length && ( +
+ {lang('Story.ViewList.EmptyTextSearch')} +
+ )} + + {sortedViewIds?.map((id) => ( + + ))} + {isLoading && Array.from({ length: placeholderCount }).map((_, i) => ( + + + + ))} + {notAllAvailable && ( +
+ {lang('Story.ViewList.NotFullyRecorded')} +
+ )} +
+
+
+ +
+
+ ); +} + +function prepareComparator(areReactionsFirst?: boolean) { + return (a: ApiStoryView, b: ApiStoryView) => { + if (areReactionsFirst) { + if (a.reaction && !b.reaction) { + return -1; + } + if (!a.reaction && b.reaction) { + return 1; + } + } + + return b.date - a.date; + }; +} + +export default memo(withGlobal((global) => { + const { appConfig } = global; + const { storyViewer: { viewModal } } = selectTabState(global); + const { + storyId, viewsById, nextOffset, isLoading, + } = viewModal || {}; + const story = storyId ? selectUserStory(global, global.currentUserId!, storyId) : undefined; + + return { + storyId, + viewsById, + viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, + story: story && 'content' in story ? story : undefined, + nextOffset, + isLoading, + availableReactions: global.availableReactions, + isCurrentUserPremium: selectIsCurrentUserPremium(global), + }; +})(StoryViewModal)); diff --git a/src/components/story/StoryViewer.module.scss b/src/components/story/StoryViewer.module.scss index 20c32d564..6e8a6cadc 100644 --- a/src/components/story/StoryViewer.module.scss +++ b/src/components/story/StoryViewer.module.scss @@ -1,6 +1,8 @@ @import "../../styles/mixins"; .root { + --color-story-meta: rgb(242, 242, 242); + position: fixed; top: 0; left: 0; @@ -325,8 +327,8 @@ left: 0; top: 0; right: 0; - height: 4.5rem; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); + height: 5rem; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); z-index: 1; border-radius: var(--border-radius-default-small) var(--border-radius-default-small) 0 0; } @@ -389,10 +391,10 @@ .storyMeta { font-size: 0.875rem; - opacity: 0.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: var(--color-story-meta); & + & { margin-left: 0.375rem; @@ -411,6 +413,10 @@ } } +.topIcon { + color: var(--color-white); +} + .actions { margin-inline-start: auto; display: flex; @@ -466,64 +472,68 @@ } } +.captionGradient { + position: absolute; + bottom: 3.5rem; + left: 0; + right: 0; + height: 7rem; + background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 100%); + pointer-events: none; +} + .caption { position: absolute; - bottom: calc(3.5rem - 1px); + bottom: 5rem; left: 0; width: 100%; + top: 4rem; display: flex; flex-direction: column; + justify-content: flex-end; border-radius: 0 0 var(--border-radius-default-small) var(--border-radius-default-small); - max-height: 35%; overflow: hidden; + pointer-events: none; - @media (max-width: 600px) { - bottom: 4rem; - border-radius: 0; + @supports (bottom: env(safe-area-inset-bottom)) { + bottom: calc(5rem + env(safe-area-inset-bottom)); } } .captionInner { + position: relative; word-break: break-word; white-space: pre-wrap; - line-height: 1.3125; + line-height: 1.25rem; text-align: initial; unicode-bidi: plaintext; - padding: 1rem 1rem 0; - margin-bottom: 1rem; + padding: 2rem 1rem 0; overflow-x: hidden; overflow-y: scroll; - @include adapt-padding-to-scrollbar(1rem); + scrollbar-gutter: stable; + + @include adapt-padding-to-scrollbar(2rem); } .captionContent { width: 100%; color: var(--color-white); font-size: var(--message-text-size, 1rem); - background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.6) 100%); display: flex; flex-direction: column; min-height: 0; - - &:not(&.expanded) { - .captionInner { - max-height: 3.5rem; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - scrollbar-gutter: stable; - } - } + pointer-events: all; } .hasOverflow { - transform: translateY(calc(100% - 4.4375rem)); + transform: translateY(calc(100% - 5.75rem)); } .expanded { transition: transform 400ms; + @include gradient-border-top(2rem); + &::before { opacity: 1; } @@ -533,21 +543,28 @@ transform: translateY(0) !important; } -.captionInteractive { +.withShowMore { cursor: var(--custom-cursor, pointer); + + .captionInner { + overflow-y: hidden; + + mask-image: linear-gradient(to top, black 0%, black 0%), linear-gradient(to left, black 75%, transparent 100%); + mask-position: 100% 100%, 100% 4.5rem; + mask-size: 100% 100%, calc(var(--expand-button-width, 0%) + 4rem) 1.25em; + mask-repeat: no-repeat; + -webkit-mask-composite: xor; + mask-composite: exclude; + } } -.captionExpand { - float: right; - margin-bottom: 0.125rem; - padding: 0.5rem 1rem; - transition: opacity 200ms; - - @include adapt-padding-to-scrollbar(1rem); - - &.hidden { - opacity: 0; - } +.captionShowMore { + position: absolute; + bottom: -0.25rem; + right: 0.5rem; + color: var(--color-white); + font-weight: 500; + cursor: var(--custom-cursor, pointer); } .composer { @@ -568,8 +585,8 @@ @media (max-width: 600px) { padding: 0 0.5rem 0.5rem; - @supports (padding-bottom: env(safe-area-inset-bottom)) { - padding-bottom: max(env(safe-area-inset-bottom), 0.5rem); + @supports (margin-bottom: env(safe-area-inset-bottom)) { + margin-bottom: env(safe-area-inset-bottom); } } } @@ -587,14 +604,6 @@ --color-background-selected: rgba(0, 0, 0, 0.2); } - :global(.main-button) { - --color-composer-button: #fff; - } - - :global(.main-button .icon) { - --color-primary: #fff; - } - :global(.composer-wrapper) { max-width: 100%; } @@ -635,14 +644,20 @@ 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; - padding: 0.25rem; + transition: background-color 200ms, opacity 350ms !important; + padding: 0.5rem; border-radius: var(--border-radius-default); color: #fff; } @@ -651,12 +666,24 @@ cursor: var(--custom-cursor, pointer); &:hover { - background: var(--color-interactive-element-hover); + background-color: rgba(var(--color-text-secondary-rgb), 0.2); } } -.recentViewersCount { +.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) { @@ -668,16 +695,6 @@ } } -.seenByList { - min-height: 8rem; - display: flex; - flex-direction: column; -} - -.seenByListLoading { - justify-content: center; -} - .thumbnail { position: absolute; top: 0; @@ -688,16 +705,19 @@ object-fit: cover; } -.skeleton { +.mediaAreaOverlay { position: absolute; - top: 0; - left: 0; + width: auto; + left: 50%; + transform: translateX(-50%); + pointer-events: none; } -.expiredText { - color: var(--color-text-secondary); - text-align: center; - margin-block: auto; +.mediaArea { + position: absolute; + transform-origin: top left; + pointer-events: all; + cursor: var(--custom-cursor, pointer); } .ghost { diff --git a/src/components/story/StoryViewer.tsx b/src/components/story/StoryViewer.tsx index 4cd5959e9..1139cd5fd 100644 --- a/src/components/story/StoryViewer.tsx +++ b/src/components/story/StoryViewer.tsx @@ -14,26 +14,26 @@ import { } from '../../global/selectors'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager'; - +import { animateOpening, animateClosing } from './helpers/ghostAnimation'; +import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; +import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; +import buildClassName from '../../util/buildClassName'; import { ANIMATION_END_DELAY } from '../../config'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; -import useHistoryBack from '../../hooks/useHistoryBack'; import usePrevious from '../../hooks/usePrevious'; -import { useStoryProps } from './hooks/useStoryProps'; -import { useSlideSizes } from './hooks/useSlideSizes'; -import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; -import { animateOpening, animateClosing } from './helpers/ghostAnimation'; -import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; +import useStoryProps from './hooks/useStoryProps'; +import useSlideSizes from './hooks/useSlideSizes'; import ShowTransition from '../ui/ShowTransition'; import Button from '../ui/Button'; import StorySlides from './StorySlides'; import StoryDeleteConfirmModal from './StoryDeleteConfirmModal'; -import StoryViewers from './StoryViewers'; +import StoryViewModal from './StoryViewModal'; import ReportModal from '../common/ReportModal'; import StorySettings from './StorySettings'; +import StealthModeModal from './StealthModeModal'; import styles from './StoryViewer.module.scss'; @@ -92,8 +92,8 @@ function StoryViewer({ const stopPriorityPlayback = dispatchPriorityPlaybackEvent(); return () => { - stopPriorityPlayback(); enableDirectTextInput(); + stopPriorityPlayback(); }; }, [isOpen]); @@ -111,12 +111,6 @@ function StoryViewer({ setIdStoryForDelete(undefined); }, []); - useHistoryBack({ - isActive: isOpen, - onBack: handleClose, - shouldBeReplaced: true, - }); - useEffect(() => (isOpen ? captureEscKeyListener(() => { handleClose(); }) : undefined), [handleClose, isOpen]); @@ -161,10 +155,11 @@ function StoryViewer({ ariaLabel={lang('Close')} onClick={handleClose} > - + - + + ; - viewersExpirePeriod: number; - isCurrentUserPremium?: boolean; -} -const CLOSE_ANIMATION_DURATION = 100; - -function StoryViewers({ - storyId, - storyDate, - viewsCount, - viewersExpirePeriod, - seenByDates, - isCurrentUserPremium, -}: StateProps) { - const { - loadStorySeenBy, openChat, closeStorySeenBy, closeStoryViewer, - } = getActions(); - - const lang = useLang(); - - const isOpen = Boolean(storyId); - const isExpired = !isCurrentUserPremium && Boolean(storyDate) && (storyDate + viewersExpirePeriod) < getServerTime(); - const renderingSeenByDates = useCurrentOrPrev(seenByDates, true); - const renderingIsExpired = usePrevious(isExpired) || isExpired; - const renderingViewsCount = useCurrentOrPrev(viewsCount, true); - - const memberIds = useMemo(() => { - if (!renderingSeenByDates || renderingIsExpired) { - return undefined; - } - - const result = Object.keys(renderingSeenByDates); - result.sort((leftId, rightId) => renderingSeenByDates[rightId] - renderingSeenByDates[leftId]); - - return result; - }, [renderingIsExpired, renderingSeenByDates]); - const isLoading = !isCurrentUserPremium && !renderingIsExpired && (!memberIds || memberIds.length === 0); - - useEffect(() => { - if (!storyId || seenByDates || renderingIsExpired) { - return; - } - - // TODO Infinite scroll - loadStorySeenBy({ storyId }); - }, [renderingIsExpired, seenByDates, storyId]); - - const handleCloseSeenByModal = useLastCallback(() => { - closeStorySeenBy(); - }); - - const handleClick = useLastCallback((userId: string) => { - closeStorySeenBy(); - closeStoryViewer(); - - setTimeout(() => { - openChat({ id: userId }); - }, CLOSE_ANIMATION_DURATION); - }); - - return ( - -
- {isLoading && } - {renderingIsExpired && ( -
- {renderText(lang('ExpiredViewsStub'), ['simple_markdown', 'emoji'])} -
- )} - {isCurrentUserPremium && Boolean(!memberIds?.length) && ( -
- {lang('ServerErrorViewers')} -
- )} - {memberIds?.map((userId) => ( - handleClick(userId)} - > - - - ))} -
-
- -
-
- ); -} - -export default memo(withGlobal((global) => { - const { appConfig } = global; - const { storyViewer: { storyIdSeenBy } } = selectTabState(global); - const story = storyIdSeenBy ? selectUserStory(global, global.currentUserId!, storyIdSeenBy) : undefined; - const storyDate = story && 'date' in story ? story.date : undefined; - const viewsCount = story && 'viewsCount' in story ? story.viewsCount : undefined; - - return { - storyId: storyIdSeenBy, - seenByDates: storyIdSeenBy ? selectStorySeenBy(global, global.currentUserId!, storyIdSeenBy) : undefined, - viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod, - storyDate, - viewsCount, - isCurrentUserPremium: selectIsCurrentUserPremium(global), - }; -})(StoryViewers)); diff --git a/src/components/story/helpers/dimensions.ts b/src/components/story/helpers/dimensions.ts index fcbbf6a75..74e561193 100644 --- a/src/components/story/helpers/dimensions.ts +++ b/src/components/story/helpers/dimensions.ts @@ -2,7 +2,7 @@ import type { IDimensions } from '../../../global/types'; const BASE_SCREEN_WIDTH = 1200; const BASE_SCREEN_HEIGHT = 800; -const BASE_ACTIVE_SLIDE_WIDTH = 404; +const BASE_ACTIVE_SLIDE_WIDTH = 405; const BASE_ACTIVE_SLIDE_HEIGHT = 720; const BASE_SLIDE_WIDTH = 135; const BASE_SLIDE_HEIGHT = 240; @@ -15,14 +15,15 @@ export function calculateSlideSizes(windowWidth: number, windowHeight: number): } { const scale = calculateScale(BASE_SCREEN_WIDTH, BASE_SCREEN_HEIGHT, windowWidth, windowHeight); + // Avoid fractional values to prevent blurry text return { activeSlide: { - width: BASE_ACTIVE_SLIDE_WIDTH * scale, - height: BASE_ACTIVE_SLIDE_HEIGHT * scale, + width: roundToNearestEven(BASE_ACTIVE_SLIDE_WIDTH * scale), + height: roundToNearestEven(BASE_ACTIVE_SLIDE_HEIGHT * scale), }, slide: { - width: BASE_SLIDE_WIDTH * scale, - height: BASE_SLIDE_HEIGHT * scale, + width: roundToNearestEven(BASE_SLIDE_WIDTH * scale), + height: roundToNearestEven(BASE_SLIDE_HEIGHT * scale), }, scale, }; @@ -44,7 +45,7 @@ export function calculateOffsetX({ const mainOffset = BASE_GAP_WIDTH + (isActiveSlideSize ? BASE_ACTIVE_SLIDE_WIDTH : BASE_SLIDE_WIDTH); const additionalOffset = (Math.abs(slideAmount) - 1) * ((isMoveThroughActiveSlide ? BASE_ACTIVE_SLIDE_WIDTH : BASE_SLIDE_WIDTH) + BASE_GAP_WIDTH); - const totalOffset = (mainOffset + additionalOffset) * scale; + const totalOffset = roundToNearestEven((mainOffset + additionalOffset) * scale); return isBackward ? -totalOffset : totalOffset; } @@ -55,3 +56,8 @@ function calculateScale(baseWidth: number, baseHeight: number, newWidth: number, return Math.min(widthScale, heightScale); } + +// Fractional values cause blurry text. Round to even to keep whole numbers while centering +function roundToNearestEven(value: number) { + return Math.round(value / 2) * 2; +} diff --git a/src/components/story/hooks/useSlideSizes.ts b/src/components/story/hooks/useSlideSizes.ts index cd70dc439..be19d701c 100644 --- a/src/components/story/hooks/useSlideSizes.ts +++ b/src/components/story/hooks/useSlideSizes.ts @@ -2,7 +2,7 @@ import useWindowSize from '../../../hooks/useWindowSize'; import { useMemo } from '../../../lib/teact/teact'; import { calculateSlideSizes } from '../helpers/dimensions'; -export const useSlideSizes = () => { +export default function useSlideSizes() { const { width: windowWidth, height: windowHeight } = useWindowSize(); return useMemo(() => calculateSlideSizes(windowWidth, windowHeight), [windowWidth, windowHeight]); -}; +} diff --git a/src/components/story/hooks/useStoryProps.ts b/src/components/story/hooks/useStoryProps.ts index a18e7b8c3..9b3932cd0 100644 --- a/src/components/story/hooks/useStoryProps.ts +++ b/src/components/story/hooks/useStoryProps.ts @@ -3,7 +3,9 @@ import { hasMessageText, getStoryMediaHash } from '../../../global/helpers'; import useMedia from '../../../hooks/useMedia'; import { ApiMediaFormat } from '../../../api/types'; -export const useStoryProps = (story?: ApiTypeStory) => { +export default function useStoryProps( + story?: ApiTypeStory, isCurrentUserPremium = false, isDropdownMenuOpen = false, +) { const isLoadedStory = story && 'content' in story; const isDeletedStory = story && 'isDeleted' in story; const hasText = isLoadedStory ? hasMessageText(story) : false; @@ -30,6 +32,11 @@ export const useStoryProps = (story?: ApiTypeStory) => { const hasFullData = Boolean(fullMediaData || altMediaData); const bestImageData = isVideo ? previewBlobUrl : fullMediaData || previewBlobUrl; const hasThumb = !previewBlobUrl && !hasFullData; + const mediaAreas = isLoadedStory ? story.mediaAreas : undefined; + + const canDownload = isCurrentUserPremium && isLoadedStory && !story.noForwards; + const downloadHash = isLoadedStory ? getStoryMediaHash(story, 'download') : undefined; + const downloadMediaData = useMedia(downloadHash, !canDownload && !isDropdownMenuOpen); return { isLoadedStory, @@ -47,5 +54,8 @@ export const useStoryProps = (story?: ApiTypeStory) => { hasFullData, bestImageData, hasThumb, + mediaAreas, + canDownload, + downloadMediaData, }; -}; +} diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index 20dae3f72..1cd6c20d5 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -5,6 +5,7 @@ import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment'; import buildClassName from '../../util/buildClassName'; +import renderText from '../common/helpers/renderText'; import useLastCallback from '../../hooks/useLastCallback'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; @@ -59,6 +60,7 @@ interface OwnProps { isStatic?: boolean; contextActions?: MenuItemContextAction[]; withPortalForMenu?: boolean; + menuBubbleClassName?: string; href?: string; onMouseDown?: (e: React.MouseEvent) => void; onClick?: (e: React.MouseEvent, arg?: any) => void; @@ -74,6 +76,7 @@ const ListItem: FC = ({ icon, leftElement, buttonClassName, + menuBubbleClassName, secondaryIcon, rightElement, className, @@ -260,11 +263,12 @@ const ListItem: FC = ({ positionX={positionX} positionY={positionY} style={menuStyle} - className="ListItem-context-menu" + className="ListItem-context-menu with-menu-transitions" autoClose onClose={handleContextMenuClose} onCloseAnimationEnd={handleContextMenuHide} withPortal={withPortalForMenu} + bubbleClassName={menuBubbleClassName} > {contextActions.map((action) => ( ('isSeparator' in action) ? ( @@ -277,7 +281,9 @@ const ListItem: FC = ({ disabled={!action.handler} onClick={action.handler} > - {action.title} + + {renderText(action.title)} + ) ))} diff --git a/src/components/ui/Menu.scss b/src/components/ui/Menu.scss index 1c4cbdc17..2ffdb91f2 100644 --- a/src/components/ui/Menu.scss +++ b/src/components/ui/Menu.scss @@ -109,4 +109,8 @@ margin: 0.125rem 1rem; width: calc(100% - 2rem); } + + &.in-portal { + z-index: var(--z-portal-menu); + } } diff --git a/src/components/ui/Menu.tsx b/src/components/ui/Menu.tsx index da3aa8596..9ae311721 100644 --- a/src/components/ui/Menu.tsx +++ b/src/components/ui/Menu.tsx @@ -1,6 +1,6 @@ -import type { RefObject } from 'react'; -import type { FC } from '../../lib/teact/teact'; -import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; +import React, { + type FC, memo, useEffect, useRef, +} from '../../lib/teact/teact'; import { IS_BACKDROP_BLUR_SUPPORTED } from '../../util/windowEnvironment'; import captureEscKeyListener from '../../util/captureEscKeyListener'; @@ -8,6 +8,8 @@ import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; +import freezeWhenClosed from '../../util/hoc/freezeWhenClosed'; + import useShowTransition from '../../hooks/useShowTransition'; import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation'; import useVirtualBackdrop from '../../hooks/useVirtualBackdrop'; @@ -20,8 +22,8 @@ import Portal from './Portal'; import './Menu.scss'; type OwnProps = { - ref?: RefObject; - containerRef?: RefObject; + ref?: React.RefObject; + containerRef?: React.RefObject; isOpen: boolean; shouldCloseFast?: boolean; id?: string; @@ -146,6 +148,7 @@ const Menu: FC = ({ 'Menu', !noCompact && !isTouchScreen && 'compact', !IS_BACKDROP_BLUR_SUPPORTED && 'no-blur', + withPortal && 'in-portal', className, )} style={style} @@ -186,4 +189,4 @@ const Menu: FC = ({ return menu; }; -export default memo(Menu); +export default memo(freezeWhenClosed(Menu)); diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index f9c376152..70d20ca94 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,7 +1,6 @@ -import type { RefObject } from 'react'; -import type { FC, TeactNode } from '../../lib/teact/teact'; import React, { useEffect, useRef } from '../../lib/teact/teact'; +import type { FC, TeactNode } from '../../lib/teact/teact'; import type { TextPart } from '../../types'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; @@ -9,6 +8,7 @@ import trapFocus from '../../util/trapFocus'; import buildClassName from '../../util/buildClassName'; import { enableDirectTextInput, disableDirectTextInput } from '../../util/directInputManager'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; +import freezeWhenClosed from '../../util/hoc/freezeWhenClosed'; import useLastCallback from '../../hooks/useLastCallback'; import useShowTransition from '../../hooks/useShowTransition'; @@ -26,6 +26,7 @@ const ANIMATION_DURATION = 200; type OwnProps = { title?: string | TextPart[]; className?: string; + contentClassName?: string; isOpen?: boolean; header?: TeactNode; isSlim?: boolean; @@ -37,7 +38,7 @@ type OwnProps = { onClose: () => void; onCloseAnimationEnd?: () => void; onEnter?: () => void; - dialogRef?: RefObject; + dialogRef?: React.RefObject; }; type StateProps = { @@ -48,6 +49,7 @@ const Modal: FC = ({ dialogRef, title, className, + contentClassName, isOpen, isSlim, header, @@ -165,7 +167,7 @@ const Modal: FC = ({
{renderHeader()} -
+
{children}
@@ -175,4 +177,4 @@ const Modal: FC = ({ ); }; -export default Modal; +export default freezeWhenClosed(Modal); diff --git a/src/components/ui/TextTimer.tsx b/src/components/ui/TextTimer.tsx new file mode 100644 index 000000000..ee7378b48 --- /dev/null +++ b/src/components/ui/TextTimer.tsx @@ -0,0 +1,44 @@ +import React, { type FC, memo, useEffect } from '../../lib/teact/teact'; + +import { getServerTime } from '../../util/serverTime'; +import { formatMediaDuration } from '../../util/dateFormat'; + +import useForceUpdate from '../../hooks/useForceUpdate'; +import useLang from '../../hooks/useLang'; +import useInterval from '../../hooks/useInterval'; + +type OwnProps = { + langKey: string; + endsAt: number; + onEnd?: NoneToVoidFunction; +}; + +const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000 + +const TextTimer: FC = ({ langKey, endsAt, onEnd }) => { + const lang = useLang(); + const forceUpdate = useForceUpdate(); + + const serverTime = getServerTime(); + const isActive = serverTime < endsAt; + useInterval(forceUpdate, isActive ? UPDATE_FREQUENCY : undefined); + + useEffect(() => { + if (!isActive) { + onEnd?.(); + } + }, [isActive, onEnd]); + + if (!isActive) return undefined; + + const timeLeft = endsAt - serverTime; + const formattedTime = formatMediaDuration(timeLeft); + + return ( + + {lang(langKey, formattedTime)} + + ); +}; + +export default memo(TextTimer); diff --git a/src/components/ui/placeholder/PlaceholderChatInfo.module.scss b/src/components/ui/placeholder/PlaceholderChatInfo.module.scss new file mode 100644 index 000000000..8563c3297 --- /dev/null +++ b/src/components/ui/placeholder/PlaceholderChatInfo.module.scss @@ -0,0 +1,50 @@ +.root { + display: flex; + gap: 0.5rem; + width: 100%; +} + +.avatar { + width: 2.75rem; + height: 2.75rem; + border-radius: 50%; + flex-shrink: 0; +} + +.info { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 0.5rem; + justify-content: center; +} + +.title { + width: 75%; + height: 1rem; + border-radius: 0.5rem; +} + +.subtitle { + width: 50%; + height: 0.875rem; + border-radius: 0.5rem; +} + +.animated { + background: linear-gradient( + 90deg, + var(--color-skeleton-background), + var(--color-skeleton-foreground), + var(--color-skeleton-background) + ); + background-size: 100vw 100vh; + background-attachment: fixed; + animation: slide 3s infinite linear; +} + +@keyframes slide { + to { + background-position: 100vw 0; + } +} diff --git a/src/components/ui/placeholder/PlaceholderChatInfo.tsx b/src/components/ui/placeholder/PlaceholderChatInfo.tsx new file mode 100644 index 000000000..f786c7b48 --- /dev/null +++ b/src/components/ui/placeholder/PlaceholderChatInfo.tsx @@ -0,0 +1,19 @@ +import React, { memo } from '../../../lib/teact/teact'; + +import buildClassName from '../../../util/buildClassName'; + +import styles from './PlaceholderChatInfo.module.scss'; + +const PlaceholderChatInfo = () => { + return ( +
+
+
+
+
+
+
+ ); +}; + +export default memo(PlaceholderChatInfo); diff --git a/src/components/ui/Skeleton.scss b/src/components/ui/placeholder/Skeleton.scss similarity index 100% rename from src/components/ui/Skeleton.scss rename to src/components/ui/placeholder/Skeleton.scss diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/placeholder/Skeleton.tsx similarity index 81% rename from src/components/ui/Skeleton.tsx rename to src/components/ui/placeholder/Skeleton.tsx index c33875321..25f5dd9cf 100644 --- a/src/components/ui/Skeleton.tsx +++ b/src/components/ui/placeholder/Skeleton.tsx @@ -1,9 +1,9 @@ -import React from '../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; -import type { FC } from '../../lib/teact/teact'; +import type { FC } from '../../../lib/teact/teact'; -import buildClassName from '../../util/buildClassName'; -import buildStyle from '../../util/buildStyle'; +import buildClassName from '../../../util/buildClassName'; +import buildStyle from '../../../util/buildStyle'; import './Skeleton.scss'; diff --git a/src/config.ts b/src/config.ts index 3917115ba..b29cf4037 100644 --- a/src/config.ts +++ b/src/config.ts @@ -49,7 +49,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false; export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview'; export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; -export const LANG_CACHE_NAME = 'tt-lang-packs-v20'; +export const LANG_CACHE_NAME = 'tt-lang-packs-v21'; export const ASSET_CACHE_NAME = 'tt-assets'; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; @@ -86,6 +86,10 @@ export const COMMON_CHATS_LIMIT = 100; export const GROUP_CALL_PARTICIPANTS_LIMIT = 100; export const STORY_LIST_LIMIT = 100; +export const STORY_VIEWS_MIN_SEARCH = 15; +export const STORY_MIN_REACTIONS_SORT = 10; +export const STORY_VIEWS_MIN_CONTACTS_FILTER = 20; + // As in Telegram for Android // https://github.com/DrKLO/Telegram/blob/51e9947527/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java#L7799 export const TOP_REACTIONS_LIMIT = 100; @@ -126,9 +130,11 @@ export const APP_CONFIG_REFETCH_INTERVAL = 10000 * 1000; export const EDITABLE_INPUT_ID = 'editable-message-text'; export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal'; +export const EDITABLE_STORY_INPUT_ID = 'editable-story-input-text'; // eslint-disable-next-line max-len export const EDITABLE_INPUT_CSS_SELECTOR = `.messages-layout .Transition_slide-active #${EDITABLE_INPUT_ID}, .messages-layout .Transition > .Transition_slide-to #${EDITABLE_INPUT_ID}`; export const EDITABLE_INPUT_MODAL_CSS_SELECTOR = `#${EDITABLE_INPUT_MODAL_ID}`; +export const EDITABLE_STORY_INPUT_CSS_SELECTOR = `#${EDITABLE_STORY_INPUT_ID}`; export const CUSTOM_APPENDIX_ATTRIBUTE = 'data-has-custom-appendix'; export const MESSAGE_CONTENT_CLASS_NAME = 'message-content'; @@ -254,7 +260,7 @@ export const SUPPORTED_TRANSLATION_LANGUAGES = [ ]; // eslint-disable-next-line max-len -export const RE_LINK_TEMPLATE = '((ftp|https?):\\/\\/)?((www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,63})\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)'; +export const RE_LINK_TEMPLATE = '((ftp|https?):\\/\\/)?((www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,63})\\b([-a-zA-Z0-9()@:%_+.,~#?&/=]*)'; export const RE_MENTION_TEMPLATE = '(@[\\w\\d_-]+)'; export const RE_TG_LINK = /^tg:(\/\/)?/i; export const RE_TME_LINK = /^(https?:\/\/)?([-a-zA-Z0-9@:%_+~#=]{1,32}\.)?t\.me/i; diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index f54a65fa2..89e7895e2 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -15,7 +15,7 @@ import { selectChat, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectTabState, selectBot, selectIsTrustedBot, selectReplyingToId, selectSendAs, selectUser, selectThreadTopMessageId, selectUserFullInfo, } from '../../selectors'; -import { addChats, addUsers, removeBlockedContact } from '../../reducers'; +import { addChats, addUsers, removeBlockedUser } from '../../reducers'; import { buildCollectionByKey } from '../../../util/iteratees'; import { debounce } from '../../../util/schedulers'; import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots'; @@ -201,13 +201,13 @@ addActionHandler('restartBot', async (global, actions, payload): Promise = return; } - const result = await callApi('unblockContact', bot.id, bot.accessHash); + const result = await callApi('unblockUser', { user: bot }); if (!result) { return; } global = getGlobal(); - global = removeBlockedContact(global, bot.id); + global = removeBlockedUser(global, bot.id); setGlobal(global); void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId)); }); @@ -408,7 +408,7 @@ addActionHandler('startBot', async (global, actions, payload): Promise => } if (fullInfo?.isBlocked) { - await callApi('unblockContact', bot.id, bot.accessHash); + await callApi('unblockUser', { user: bot }); } await callApi('startBot', { diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 6e51c74e7..f4cf80205 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -63,7 +63,7 @@ import { selectChatFolder, selectSupportChat, selectChatByUsername, selectCurrentMessageList, selectThreadInfo, selectCurrentChat, selectLastServiceNotification, selectVisibleUsers, selectUserByPhoneNumber, selectDraft, selectThreadTopMessageId, - selectTabState, selectThreadOriginChat, selectThread, selectChatFullInfo, + selectTabState, selectThreadOriginChat, selectThread, selectChatFullInfo, selectStickerSet, } from '../../selectors'; import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { debounce, pause, throttle } from '../../../util/schedulers'; @@ -1624,7 +1624,7 @@ addActionHandler('setChatEnabledReactions', async (global, actions, payload): Pr void loadFullChat(global, actions, chat, tabId); }); -addActionHandler('fetchChat', async (global, actions, payload): Promise => { +addActionHandler('fetchChat', (global, actions, payload): ActionReturnType => { const { chatId } = payload; const chat = selectChat(global, chatId); @@ -2423,7 +2423,8 @@ export async function loadFullChat( setGlobal(global); const stickerSet = fullInfo.stickerSet; - if (stickerSet) { + const localSet = stickerSet && selectStickerSet(global, stickerSet); + if (stickerSet && !localSet) { actions.loadStickers({ stickerSetInfo: { id: stickerSet.id, diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 21994ca5a..e5afa4e5c 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -386,6 +386,8 @@ addActionHandler('openPremiumModal', async (global, actions, payload): Promise => { diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index a5a92cca4..ee30e0178 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -21,7 +21,7 @@ import { selectChat, selectUser, selectTabState, selectUserFullInfo, } from '../../selectors'; import { - addUsers, addBlockedContact, updateChats, updateUser, removeBlockedContact, replaceSettings, updateNotifySettings, + addUsers, addBlockedUser, updateChats, updateUser, removeBlockedUser, replaceSettings, updateNotifySettings, addNotifyExceptions, updateChat, updateUserFullInfo, } from '../../reducers'; import { isUserId } from '../../helpers'; @@ -272,11 +272,9 @@ addActionHandler('uploadWallpaper', async (global, actions, payload): Promise => { - const result = await callApi('fetchBlockedContacts'); - if (!result) { - return; - } +addActionHandler('loadBlockedUsers', async (global): Promise => { + const result = await callApi('fetchBlockedUsers', {}); + if (!result) return; global = getGlobal(); @@ -290,7 +288,6 @@ addActionHandler('loadBlockedContacts', async (global): Promise => { global = { ...global, blocked: { - ...global.blocked, ids: result.blockedIds, totalCount: result.totalCount, }, @@ -299,40 +296,37 @@ addActionHandler('loadBlockedContacts', async (global): Promise => { setGlobal(global); }); -addActionHandler('blockContact', async (global, actions, payload): Promise => { - const { contactId, accessHash } = payload!; +addActionHandler('blockUser', async (global, actions, payload): Promise => { + const { userId, isOnlyStories } = payload; - const result = await callApi('blockContact', contactId, accessHash); - if (!result) { - return; - } + const user = selectUser(global, userId); + if (!user) return; + + const result = await callApi('blockUser', { + user, + isOnlyStories: isOnlyStories || undefined, + }); + if (!result) return; global = getGlobal(); - global = addBlockedContact(global, contactId); + global = addBlockedUser(global, userId); setGlobal(global); }); -addActionHandler('unblockContact', async (global, actions, payload): Promise => { - const { contactId } = payload!; - let accessHash: string | undefined; - const isPrivate = isUserId(contactId); +addActionHandler('unblockUser', async (global, actions, payload): Promise => { + const { userId, isOnlyStories } = payload; - if (isPrivate) { - const user = selectUser(global, contactId); - if (!user) { - return; - } + const user = selectUser(global, userId); + if (!user) return; - accessHash = user.accessHash; - } - - const result = await callApi('unblockContact', contactId, accessHash); - if (!result) { - return; - } + const result = await callApi('unblockUser', { + user, + isOnlyStories: isOnlyStories || undefined, + }); + if (!result) return; global = getGlobal(); - global = removeBlockedContact(global, contactId); + global = removeBlockedUser(global, userId); setGlobal(global); }); diff --git a/src/global/actions/api/stories.ts b/src/global/actions/api/stories.ts index 5df04a472..2cb9032e4 100644 --- a/src/global/actions/api/stories.ts +++ b/src/global/actions/api/stories.ts @@ -1,6 +1,8 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { callApi } from '../../../api/gramjs'; +import type { ActionReturnType } from '../../types'; + import { DEBUG, PREVIEW_AVATAR_COUNT } from '../../../config'; import { addStories, @@ -10,18 +12,21 @@ import { toggleUserStoriesHidden, updateLastReadStoryForUser, updateLastViewedStoryForUser, - updateStorySeenBy, + updateStealthMode, + updateStoryViews, + updateStoryViewsLoading, updateUser, updateUserPinnedStory, updateUserStory, updateUsersWithStories, } from '../../reducers'; import { buildCollectionByKey } from '../../../util/iteratees'; -import { selectUser, selectUserStories, selectUserStory } from '../../selectors'; +import { + selectUser, selectUserStories, selectUserStory, +} from '../../selectors'; import { getServerTime } from '../../../util/serverTime'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { translate } from '../../../util/langProvider'; -import type { ActionReturnType } from '../../types'; const INFINITE_LOOP_MARKER = 100; @@ -60,6 +65,7 @@ addActionHandler('loadAllStories', async (global): Promise => { global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = addStories(global, result.userStories); global = updateUsersWithStories(global, result.userStories); + global = updateStealthMode(global, result.stealthMode); global.stories.hasNext = result.hasMore; } @@ -103,6 +109,7 @@ addActionHandler('loadAllHiddenStories', async (global): Promise => { global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = addStories(global, result.userStories); global = updateUsersWithStories(global, result.userStories); + global = updateStealthMode(global, result.stealthMode); global.stories.hasNextInArchive = result.hasMore; } @@ -199,14 +206,14 @@ addActionHandler('toggleStoryPinned', async (global, actions, payload): Promise< const story = selectUserStory(global, global.currentUserId!, storyId); const currentIsPinned = story && 'content' in story ? story.isPinned : undefined; - global = updateUserStory(global, global.currentUserId!, { id: storyId, isPinned }); + global = updateUserStory(global, global.currentUserId!, storyId, { isPinned }); global = updateUserPinnedStory(global, global.currentUserId!, storyId, isPinned); setGlobal(global); const result = await callApi('toggleStoryPinned', { storyId, isPinned }); if (!result) { global = getGlobal(); - global = updateUserStory(global, global.currentUserId!, { id: storyId, isPinned: currentIsPinned }); + global = updateUserStory(global, global.currentUserId!, storyId, { isPinned: currentIsPinned }); global = updateUserPinnedStory(global, global.currentUserId!, storyId, currentIsPinned); setGlobal(global); } @@ -284,25 +291,54 @@ addActionHandler('loadUserStoriesByIds', async (global, actions, payload): Promi setGlobal(global); }); -addActionHandler('loadStorySeenBy', async (global, actions, payload): Promise => { - const { storyId, offsetId } = payload; +addActionHandler('loadStoryViews', async (global, actions, payload): Promise => { + const { + storyId, + tabId = getCurrentTabId(), + } = payload; + const isPreload = 'isPreload' in payload; + const { + offset, areReactionsFirst, areJustContacts, query, limit, + } = isPreload ? { + offset: undefined, + areReactionsFirst: undefined, + areJustContacts: undefined, + query: undefined, + limit: PREVIEW_AVATAR_COUNT, + } : payload; - const result = await callApi('fetchStorySeenBy', { storyId, offsetId }); + if (!isPreload) { + global = updateStoryViewsLoading(global, true, tabId); + setGlobal(global); + } + + const result = await callApi('fetchStoryViewList', { + storyId, + offset, + areReactionsFirst, + areJustContacts, + limit, + query, + }); if (!result) { + global = getGlobal(); + global = updateStoryViewsLoading(global, false, tabId); + setGlobal(global); return; } + const viewsById = buildCollectionByKey(result.views, 'userId'); + global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = updateStorySeenBy(global, global.currentUserId!, storyId, result.seenByDates); + if (!isPreload) global = updateStoryViews(global, storyId, viewsById, result.nextOffset, tabId); - const viewerIds = Object.keys(result.seenByDates); - if (!offsetId && viewerIds.length) { - const recentViewerIds = viewerIds.slice(-PREVIEW_AVATAR_COUNT).reverse(); - global = updateUserStory(global, global.currentUserId!, { - id: storyId, + if (isPreload && result.views?.length) { + const recentViewerIds = result.views.map((view) => view.userId); + global = updateUserStory(global, global.currentUserId!, storyId, { recentViewerIds, - viewsCount: result.count, + viewsCount: result.viewsCount, + reactionsCount: result.reactionsCount, }); } setGlobal(global); @@ -390,3 +426,38 @@ addActionHandler('loadStoriesMaxIds', async (global, actions, payload): Promise< userIdsToLoad?.forEach((userId) => actions.loadUserStories({ userId })); }); + +addActionHandler('sendStoryReaction', async (global, actions, payload): Promise => { + const { + userId, storyId, reaction, shouldAddToRecent, + } = payload; + const user = selectUser(global, userId); + if (!user) return; + + const story = selectUserStory(global, userId, storyId); + if (!story || !('content' in story)) return; + + const previousReaction = story.sentReaction; + global = updateUserStory(global, userId, storyId, { + sentReaction: reaction, + }); + setGlobal(global); + + const result = await callApi('sendStoryReaction', { + user, storyId, reaction, shouldAddToRecent, + }); + + global = getGlobal(); + if (!result) { + global = updateUserStory(global, userId, storyId, { + sentReaction: previousReaction, + }); + } + setGlobal(global); +}); + +addActionHandler('activateStealthMode', (global, actions, payload): ActionReturnType => { + const { isForPast = true, isForFuture = true } = payload || {}; + + callApi('activateStealthMode', { isForPast: isForPast || true, isForFuture: isForFuture || true }); +}); diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index 2b8a30500..58ba40751 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -4,24 +4,28 @@ import type { ActionReturnType } from '../../types'; import { PaymentStep } from '../../../types'; import { - addBlockedContact, + addBlockedUser, addStoriesForUser, - removeBlockedContact, + removeBlockedUser, removeUserStory, setConfirmPaymentUrl, setPaymentStep, updateLastReadStoryForUser, + updateStealthMode, + updateUserStory, updateUsersWithStories, } from '../../reducers'; -import { selectUserStories } from '../../selectors'; +import { selectUserStories, selectUserStory } from '../../selectors'; addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { switch (update['@type']) { case 'updatePeerBlocked': if (update.isBlocked) { - return addBlockedContact(global, update.id); + return addBlockedUser(global, update.id); + } else if (update.isBlockedFromStories) { + return global; // Unsupported } else { - return removeBlockedContact(global, update.id); + return removeBlockedUser(global, update.id); } case 'updateResetContactList': @@ -124,6 +128,20 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { global = updateLastReadStoryForUser(global, update.userId, update.lastReadId); setGlobal(global); break; + + case 'updateSentStoryReaction': { + const { userId, storyId, reaction } = update; + const story = selectUserStory(global, userId, storyId); + if (!story) return global; + global = updateUserStory(global, userId, storyId, { sentReaction: reaction }); + setGlobal(global); + break; + } + + case 'updateStealthMode': + global = updateStealthMode(global, update.stealthMode); + setGlobal(global); + break; } return undefined; diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index baa4cfc05..f49db717d 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -590,6 +590,25 @@ addActionHandler('updateArchiveSettings', (global, actions, payload): ActionRetu }; }); +addActionHandler('openMapModal', (global, actions, payload): ActionReturnType => { + const { geoPoint, zoom, tabId = getCurrentTabId() } = payload; + + return updateTabState(global, { + mapModal: { + point: geoPoint, + zoom, + }, + }, tabId); +}); + +addActionHandler('closeMapModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + mapModal: undefined, + }, tabId); +}); + addActionHandler('checkAppVersion', (global): ActionReturnType => { if (IS_ELECTRON) { return; diff --git a/src/global/actions/ui/reactions.ts b/src/global/actions/ui/reactions.ts index a14d7349f..61ff74084 100644 --- a/src/global/actions/ui/reactions.ts +++ b/src/global/actions/ui/reactions.ts @@ -49,6 +49,7 @@ addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionRe storyUserId, storyId, position, + sendAsMessage, tabId = getCurrentTabId(), } = payload!; @@ -56,6 +57,7 @@ addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionRe reactionPicker: { storyUserId, storyId, + sendAsMessage, position, }, }, tabId); diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index 011059a33..67185b9f2 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -2,6 +2,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { callApi } from '../../../api/gramjs'; import type { ActionReturnType } from '../../types'; +import type { ApiStoryView } from '../../../api/types'; import { updateTabState } from '../../reducers/tabs'; import { @@ -16,7 +17,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { copyTextToClipboard } from '../../../util/clipboard'; import { fetchChatByUsername } from '../api/chats'; import { addStoriesForUser, addUsers } from '../../reducers'; -import { buildCollectionByKey } from '../../../util/iteratees'; +import { buildCollectionByKey, omit } from '../../../util/iteratees'; import * as langProvider from '../../../util/langProvider'; addActionHandler('openStoryViewer', async (global, actions, payload): Promise => { @@ -52,8 +53,8 @@ addActionHandler('openStoryViewer', async (global, actions, payload): Promise selectTabState(global, Number(id)).storyViewer.userId); - if (!hasViewerOpen) { - global = { - ...global, - stories: { - ...global.stories, - seenByDates: undefined, - }, - }; - } - return global; }); @@ -256,27 +245,28 @@ addActionHandler('openNextStory', (global, actions, payload): ActionReturnType = }, tabId); }); -addActionHandler('openStorySeenBy', (global, actions, payload): ActionReturnType => { +addActionHandler('openStoryViewModal', (global, actions, payload): ActionReturnType => { const { storyId, tabId = getCurrentTabId() } = payload; const tabState = selectTabState(global, tabId); return updateTabState(global, { storyViewer: { ...tabState.storyViewer, - storyIdSeenBy: storyId, + viewModal: { + storyId, + nextOffset: '', + isLoading: true, + }, }, }, tabId); }); -addActionHandler('closeStorySeenBy', (global, actions, payload): ActionReturnType => { +addActionHandler('closeStoryViewModal', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; const tabState = selectTabState(global, tabId); return updateTabState(global, { - storyViewer: { - ...tabState.storyViewer, - storyIdSeenBy: undefined, - }, + storyViewer: omit(tabState.storyViewer, ['viewModal']), }, tabId); }); @@ -354,3 +344,65 @@ addActionHandler('closeStoryPrivacyEditor', (global, actions, payload): ActionRe }, }, tabId); }); + +addActionHandler('toggleStealthModal', (global, actions, payload): ActionReturnType => { + const { isOpen, tabId = getCurrentTabId() } = payload || {}; + const tabState = selectTabState(global, tabId); + + return updateTabState(global, { + storyViewer: { + ...tabState.storyViewer, + isStealthModalOpen: isOpen, + }, + }, tabId); +}); + +addActionHandler('clearStoryViews', (global, actions, payload): ActionReturnType => { + const { isLoading, tabId = getCurrentTabId() } = payload || {}; + + const tabState = selectTabState(global, tabId); + + if (!tabState.storyViewer.viewModal) return global; + + return updateTabState(global, { + storyViewer: { + ...tabState.storyViewer, + viewModal: { + ...tabState.storyViewer.viewModal, + viewsById: {}, + isLoading, + nextOffset: '', + }, + }, + }, tabId); +}); + +addActionHandler('updateStoryView', (global, actions, payload): ActionReturnType => { + const { + userId, isUserBlocked, areStoriesBlocked, tabId = getCurrentTabId(), + } = payload; + + const tabState = selectTabState(global, tabId); + const { viewModal } = tabState.storyViewer; + + if (!viewModal?.viewsById?.[userId]) return global; + + const updatedViewsById: Record = { + ...viewModal.viewsById, + [userId]: { + ...viewModal.viewsById[userId], + isUserBlocked: isUserBlocked || undefined, + areStoriesBlocked: areStoriesBlocked || undefined, + }, + }; + + return updateTabState(global, { + storyViewer: { + ...tabState.storyViewer, + viewModal: { + ...viewModal, + viewsById: updatedViewsById, + }, + }, + }, tabId); +}); diff --git a/src/global/cache.ts b/src/global/cache.ts index 62b1c3be3..db69c0751 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -194,6 +194,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.stories) { cached.stories = initialState.stories; } + + if (!cached.stories.stealthMode) { + cached.stories.stealthMode = initialState.stories.stealthMode; + } } function updateCache() { diff --git a/src/global/helpers/media.ts b/src/global/helpers/media.ts index fed4d057b..7d3a6c187 100644 --- a/src/global/helpers/media.ts +++ b/src/global/helpers/media.ts @@ -1,28 +1,55 @@ import type { ApiPhoto, ApiStory } from '../../api/types'; +import { getVideoOrAudioBaseHash } from './messageMedia'; export function getVideoAvatarMediaHash(photo: ApiPhoto) { return `videoAvatar${photo.id}?size=u`; } +type StorySize = 'pictogram' | 'preview' | 'full' | 'download'; + export function getStoryMediaHash( - story: ApiStory, size: 'pictogram' | 'preview' | 'full', isAlt: true, + story: ApiStory, size: StorySize, isAlt: true, ): string | undefined; export function getStoryMediaHash(story: ApiStory): string; -export function getStoryMediaHash(story: ApiStory, size: 'pictogram' | 'preview' | 'full'): string; +export function getStoryMediaHash(story: ApiStory, size: StorySize): string; export function getStoryMediaHash( - story: ApiStory, size: 'pictogram' | 'preview' | 'full' = 'preview', isAlt?: boolean, + story: ApiStory, size: StorySize = 'preview', isAlt?: boolean, ) { const isVideo = Boolean(story.content.video); if (isVideo) { if (isAlt && !story.content.altVideo) return undefined; - const id = isAlt ? story.content.altVideo!.id : story.content.video!.id; - return `document${id}${size === 'full' ? '' : '?size=m'}`; + const media = isAlt ? story.content.altVideo! : story.content.video!; + const id = media.id; + const base = `document${id}`; + + if (size === 'download') { + return `${base}?download`; + } + + if (size !== 'full') { + return `${base}?size=m`; + } + + return getVideoOrAudioBaseHash(media, base); } - const sizeParam = size === 'preview' - ? '?size=x' - : (size === 'pictogram' ? '?size=m' : '?size=w'); + const sizeParameter = getSizeParameter(size); - return `photo${story.content.photo!.id}${sizeParam}`; + return `photo${story.content.photo!.id}${sizeParameter}`; +} + +function getSizeParameter(size: StorySize) { + switch (size) { + case 'download': + return '?size=z'; + case 'pictogram': + return '?size=m'; + case 'preview': + return '?size=x'; + case 'full': + return '?size=w'; + default: + return ''; + } } diff --git a/src/global/helpers/messageMedia.ts b/src/global/helpers/messageMedia.ts index 741cd905a..4b7cd05bf 100644 --- a/src/global/helpers/messageMedia.ts +++ b/src/global/helpers/messageMedia.ts @@ -312,7 +312,7 @@ export function getGamePreviewVideoHash(game: ApiGame) { return undefined; } -function getVideoOrAudioBaseHash(media: ApiAudio | ApiVideo | ApiDocument, base: string) { +export function getVideoOrAudioBaseHash(media: ApiAudio | ApiVideo | ApiDocument, base: string) { if (IS_PROGRESSIVE_SUPPORTED && IS_SAFARI) { return `${base}?fileSize=${media.size}&mimeType=${media.mimeType}`; } diff --git a/src/global/helpers/renderMessageSummaryHtml.ts b/src/global/helpers/renderMessageSummaryHtml.ts index a5e8ecba4..67e9f5b41 100644 --- a/src/global/helpers/renderMessageSummaryHtml.ts +++ b/src/global/helpers/renderMessageSummaryHtml.ts @@ -10,7 +10,7 @@ export function renderMessageSummaryHtml( const emoji = getMessageSummaryEmoji(message); const emojiWithSpace = emoji ? `${emoji} ` : ''; const text = renderMessageText( - message, undefined, undefined, undefined, undefined, undefined, true, + { message, shouldRenderAsHtml: true }, )?.join(''); const description = getMessageSummaryDescription(lang, message, text, true); diff --git a/src/global/initialState.ts b/src/global/initialState.ts index fa152d654..d4e70d61c 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -116,6 +116,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { }, hasNext: true, hasNextInArchive: true, + stealthMode: {}, }, groupCalls: { diff --git a/src/global/reducers/settings.ts b/src/global/reducers/settings.ts index 59fae6366..03145ef32 100644 --- a/src/global/reducers/settings.ts +++ b/src/global/reducers/settings.ts @@ -85,7 +85,7 @@ export function updateNotifySettings( } } -export function addBlockedContact(global: T, contactId: string): T { +export function addBlockedUser(global: T, contactId: string): T { global = updateUserBlockedState(global, contactId, true); return { @@ -98,7 +98,7 @@ export function addBlockedContact(global: T, contactId: s }; } -export function removeBlockedContact(global: T, contactId: string): T { +export function removeBlockedUser(global: T, contactId: string): T { global = updateUserBlockedState(global, contactId, false); return { diff --git a/src/global/reducers/stories.ts b/src/global/reducers/stories.ts index c0cddabc3..cc232884c 100644 --- a/src/global/reducers/stories.ts +++ b/src/global/reducers/stories.ts @@ -1,6 +1,6 @@ import type { GlobalState, TabArgs } from '../types'; import type { - ApiUserStories, ApiStory, ApiStorySkipped, ApiStoryDeleted, ApiTypeStory, + ApiUserStories, ApiStory, ApiStorySkipped, ApiStoryDeleted, ApiTypeStory, ApiStoryView, ApiStealthMode, } from '../../api/types'; import { orderBy, unique } from '../../util/iteratees'; import { updateUser } from './users'; @@ -190,32 +190,54 @@ export function updateUsersWithStories( return global; } -export function updateStorySeenBy( +export function updateStoryViews( global: T, - userId: string, storyId: number, - seenByDates: Record, + viewsById: Record, + nextOffset?: string, + ...[tabId = getCurrentTabId()]: TabArgs ): T { - const currentSeenBy = global.stories.seenByDates?.[userId]?.byId[storyId] || {}; - return { - ...global, - stories: { - ...global.stories, - seenByDates: { - ...global.stories.seenByDates, - [userId]: { - ...global.stories.seenByDates?.[userId], - byId: { - ...global.stories.seenByDates?.[userId]?.byId, - [storyId]: { - ...currentSeenBy, - ...seenByDates, - }, - }, - }, + const tabState = selectTabState(global, tabId); + const { viewModal } = tabState.storyViewer; + const newViewsById = viewModal?.storyId === storyId ? { + ...viewModal.viewsById, + ...viewsById, + } : viewsById; + + global = updateStoryViewsLoading(global, false, tabId); + + return updateTabState(global, { + storyViewer: { + ...tabState.storyViewer, + viewModal: { + ...viewModal, + storyId, + viewsById: newViewsById, + nextOffset, + isLoading: false, }, }, - }; + }, tabId); +} + +export function updateStoryViewsLoading( + global: T, + isLoading: boolean, + ...[tabId = getCurrentTabId()]: TabArgs +): T { + const tabState = selectTabState(global, tabId); + const { viewModal } = tabState.storyViewer; + if (!viewModal) return global; + + return updateTabState(global, { + storyViewer: { + ...tabState.storyViewer, + viewModal: { + ...viewModal, + isLoading, + }, + }, + }, tabId); } export function removeUserStory( @@ -239,10 +261,11 @@ export function removeUserStory( [storyId]: { id: storyId, userId, isDeleted: true } as ApiStoryDeleted, }; const lastUpdatedAt = lastStoryId ? (newById[lastStoryId] as ApiStory | undefined)?.date : undefined; + const hasStories = Boolean(newOrderedIds.length); global = updateUser(global, userId, { - hasStories: newOrderedIds.length > 0, - hasUnreadStories: Boolean(lastReadId && lastStoryId && lastReadId < lastStoryId), + hasStories, + hasUnreadStories: Boolean(hasStories && lastReadId && lastStoryId && lastReadId < lastStoryId), }); global = updateStoriesForUser(global, userId, { byId: newById, @@ -258,7 +281,7 @@ export function removeUserStory( } }); - if (newOrderedIds.length === 0) { + if (!hasStories) { global = { ...global, stories: { @@ -277,7 +300,8 @@ export function removeUserStory( export function updateUserStory( global: T, userId: string, - story: Partial, + storyId: number, + storyUpdate: Partial, ): T { const userStories = selectUserStories(global, userId) || { byId: {}, orderedIds: [], pinnedIds: [], archiveIds: [], @@ -293,9 +317,9 @@ export function updateUserStory( ...userStories, byId: { ...userStories.byId, - [story.id!]: { - ...userStories.byId[story.id!], - ...story, + [storyId]: { + ...userStories.byId[storyId], + ...storyUpdate, }, }, }, @@ -346,6 +370,8 @@ function updateOrderedStoriesUserIds(global: T, updateUse const allUserIds = orderedUserIds.active.concat(orderedUserIds.archived).concat(updateUserIds); const newOrderedUserIds = allUserIds.reduce<{ active: string[]; archived: string[] }>((acc, userId) => { + if (!byUserId[userId]?.orderedIds?.length) return acc; + if (selectUser(global, userId)?.areStoriesHidden) { acc.archived.push(userId); } else { @@ -356,10 +382,15 @@ function updateOrderedStoriesUserIds(global: T, updateUse }, { active: [], archived: [] }); function sort(userId: string) { - const PREMIUM_PRIORITY = 1e12; + const UNREAD_PRIORITY = 1e12; + const PREMIUM_PRIORITY = 1e6; const isPremium = selectUser(global, userId)?.isPremium; - const lastUpdated = byUserId[userId].lastUpdatedAt || 0; - return currentUserId === userId ? Infinity : (lastUpdated + (isPremium ? PREMIUM_PRIORITY : 0)); + const { lastUpdatedAt = 0, orderedIds, lastReadId = 0 } = byUserId[userId] || {}; + const hasUnread = lastReadId < orderedIds?.[orderedIds.length - 1]; + + const priority = (hasUnread ? UNREAD_PRIORITY : 0) + (isPremium ? PREMIUM_PRIORITY : 0); + + return currentUserId === userId ? Infinity : (lastUpdatedAt + priority); } newOrderedUserIds.archived = orderBy( @@ -409,3 +440,16 @@ function updateUserLastUpdatedAt(global: T, userId: strin }, }; } + +export function updateStealthMode( + global: T, + stealthMode: ApiStealthMode, +): T { + return { + ...global, + stories: { + ...global.stories, + stealthMode, + }, + }; +} diff --git a/src/global/selectors/stories.ts b/src/global/selectors/stories.ts index 938e8f683..05636e85a 100644 --- a/src/global/selectors/stories.ts +++ b/src/global/selectors/stories.ts @@ -33,12 +33,6 @@ export function selectUserStory( return selectUserStories(global, userId)?.byId[storyId]; } -export function selectStorySeenBy( - global: T, userId: string, storyId: number, -) { - return global.stories.seenByDates?.[userId]?.byId[storyId]; -} - export function selectUserFirstUnreadStoryId( global: T, userId: string, ) { diff --git a/src/global/types.ts b/src/global/types.ts index 6596c64f4..0a05f7aaa 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -63,6 +63,9 @@ import type { ApiChatlistInvite, ApiChatlistExportedInvite, ApiUserStories, + ApiStoryView, + ApiStealthMode, + ApiGeoPoint, } from '../api/types'; import type { ApiInvoiceContainer, @@ -276,6 +279,7 @@ export type TabState = { storyUserId?: string; storyId?: number; position?: IAnchorPosition; + sendAsMessage?: boolean; }; inlineBots: { @@ -357,11 +361,17 @@ export type TabState = { isSingleStory?: boolean; isPrivate?: boolean; isArchive?: boolean; - storyIdSeenBy?: number; // Last viewed story id in current view session. // Used for better switch animation between users. lastViewedByUserIds?: Record; isPrivacyModalOpen?: boolean; + isStealthModalOpen?: boolean; + viewModal?: { + storyId: number; + viewsById?: Record; + nextOffset?: string; + isLoading?: boolean; + }; origin?: StoryViewerOrigin; }; @@ -465,6 +475,10 @@ export type TabState = { dialogs: (ApiError | ApiInviteInfo | ApiContact)[]; safeLinkModalUrl?: string; + mapModal?: { + point: ApiGeoPoint; + zoom?: number; + }; historyCalendarSelectedAt?: number; openedStickerSetShortName?: string; openedCustomEmojiSetIds?: string[]; @@ -742,9 +756,7 @@ export type GlobalState = { active: string[]; archived: string[]; }; - seenByDates?: Record>; - }>; + stealthMode: ApiStealthMode; }; groupCalls: { @@ -1018,13 +1030,16 @@ export interface ActionPayloads { currentPassword: string; onSuccess: VoidFunction; }; - loadBlockedContacts: undefined; - blockContact: { - contactId: string; - accessHash: string; + loadBlockedUsers: { + isOnlyStories?: boolean; + } | undefined; + blockUser: { + userId: string; + isOnlyStories?: boolean; }; - unblockContact: { - contactId: string; + unblockUser: { + userId: string; + isOnlyStories?: boolean; }; loadNotificationSettings: undefined; @@ -1927,6 +1942,7 @@ export interface ActionPayloads { storyUserId: string; storyId: number; position: IAnchorPosition; + sendAsMessage?: boolean; } & WithTabId; closeReactionPicker: WithTabId | undefined; @@ -1985,14 +2001,29 @@ export interface ActionPayloads { isMuted: boolean; } & WithTabId; closeStoryViewer: WithTabId | undefined; - loadStorySeenBy: { + loadStoryViews: ({ storyId: number; - offsetId?: number; + isPreload: true; + } | { + storyId: number; + offset?: string; + query?: string; + limit?: number; + areJustContacts?: true; + areReactionsFirst?: true; + }) & WithTabId; + clearStoryViews: { + isLoading?: boolean; } & WithTabId; - openStorySeenBy: { + updateStoryView: { + userId: string; + isUserBlocked?: boolean; + areStoriesBlocked?: boolean; + } & WithTabId; + openStoryViewModal: { storyId: number; } & WithTabId; - closeStorySeenBy: WithTabId | undefined; + closeStoryViewModal: WithTabId | undefined; copyStoryLink: { userId: string; storyId: number; @@ -2016,6 +2047,19 @@ export interface ActionPayloads { loadStoriesMaxIds: { userIds: string[]; }; + sendStoryReaction: { + userId: string; + storyId: number; + reaction?: ApiReaction; + shouldAddToRecent?: boolean; + }; + toggleStealthModal: { + isOpen: boolean; + } & WithTabId; + activateStealthMode: { + isForPast?: boolean; + isForFuture?: boolean; + } | undefined; // Media Viewer & Audio Player openMediaViewer: { @@ -2437,6 +2481,11 @@ export interface ActionPayloads { url: string; shouldSkipModal?: boolean; } & WithTabId; + openMapModal: { + geoPoint: ApiGeoPoint; + zoom?: number; + } & WithTabId; + closeMapModal: WithTabId | undefined; toggleSafeLinkModal: { url?: string; } & WithTabId; diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts index ee480afc9..69dc5f6fe 100644 --- a/src/hooks/useHistoryBack.ts +++ b/src/hooks/useHistoryBack.ts @@ -272,11 +272,6 @@ export default function useHistoryBack({ }, }; - // Delete forward navigation in the virtual history. Not really needed, just looks better when debugging `logState` - for (let i = indexRef.current + 1; i < historyState.length; i++) { - delete historyState[i]; - } - deferHistoryOperation({ type: shouldReplace ? 'replaceState' : 'pushState', data: { diff --git a/src/lib/gramjs/tl/AllTLObjects.js b/src/lib/gramjs/tl/AllTLObjects.js index 8bd0ff041..6f03545eb 100644 --- a/src/lib/gramjs/tl/AllTLObjects.js +++ b/src/lib/gramjs/tl/AllTLObjects.js @@ -1,6 +1,6 @@ const api = require('./api'); -const LAYER = 160; +const LAYER = 161; const tlobjects = {}; for (const tl of Object.values(api)) { diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 9edcf8c4e..96fabc956 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -86,7 +86,7 @@ namespace Api { export type TypeImportedContact = ImportedContact; export type TypeContactStatus = ContactStatus; export type TypeMessagesFilter = InputMessagesFilterEmpty | InputMessagesFilterPhotos | InputMessagesFilterVideo | InputMessagesFilterPhotoVideo | InputMessagesFilterDocument | InputMessagesFilterUrl | InputMessagesFilterGif | InputMessagesFilterVoice | InputMessagesFilterMusic | InputMessagesFilterChatPhotos | InputMessagesFilterPhoneCalls | InputMessagesFilterRoundVoice | InputMessagesFilterRoundVideo | InputMessagesFilterMyMentions | InputMessagesFilterGeo | InputMessagesFilterContacts | InputMessagesFilterPinned; - export type TypeUpdate = UpdateNewMessage | UpdateMessageID | UpdateDeleteMessages | UpdateUserTyping | UpdateChatUserTyping | UpdateChatParticipants | UpdateUserStatus | UpdateUserName | UpdateNewEncryptedMessage | UpdateEncryptedChatTyping | UpdateEncryption | UpdateEncryptedMessagesRead | UpdateChatParticipantAdd | UpdateChatParticipantDelete | UpdateDcOptions | UpdateNotifySettings | UpdateServiceNotification | UpdatePrivacy | UpdateUserPhone | UpdateReadHistoryInbox | UpdateReadHistoryOutbox | UpdateWebPage | UpdateReadMessagesContents | UpdateChannelTooLong | UpdateChannel | UpdateNewChannelMessage | UpdateReadChannelInbox | UpdateDeleteChannelMessages | UpdateChannelMessageViews | UpdateChatParticipantAdmin | UpdateNewStickerSet | UpdateStickerSetsOrder | UpdateStickerSets | UpdateSavedGifs | UpdateBotInlineQuery | UpdateBotInlineSend | UpdateEditChannelMessage | UpdateBotCallbackQuery | UpdateEditMessage | UpdateInlineBotCallbackQuery | UpdateReadChannelOutbox | UpdateDraftMessage | UpdateReadFeaturedStickers | UpdateRecentStickers | UpdateConfig | UpdatePtsChanged | UpdateChannelWebPage | UpdateDialogPinned | UpdatePinnedDialogs | UpdateBotWebhookJSON | UpdateBotWebhookJSONQuery | UpdateBotShippingQuery | UpdateBotPrecheckoutQuery | UpdatePhoneCall | UpdateLangPackTooLong | UpdateLangPack | UpdateFavedStickers | UpdateChannelReadMessagesContents | UpdateContactsReset | UpdateChannelAvailableMessages | UpdateDialogUnreadMark | UpdateMessagePoll | UpdateChatDefaultBannedRights | UpdateFolderPeers | UpdatePeerSettings | UpdatePeerLocated | UpdateNewScheduledMessage | UpdateDeleteScheduledMessages | UpdateTheme | UpdateGeoLiveViewed | UpdateLoginToken | UpdateMessagePollVote | UpdateDialogFilter | UpdateDialogFilterOrder | UpdateDialogFilters | UpdatePhoneCallSignalingData | UpdateChannelMessageForwards | UpdateReadChannelDiscussionInbox | UpdateReadChannelDiscussionOutbox | UpdatePeerBlocked | UpdateChannelUserTyping | UpdatePinnedMessages | UpdatePinnedChannelMessages | UpdateChat | UpdateGroupCallParticipants | UpdateGroupCall | UpdatePeerHistoryTTL | UpdateChatParticipant | UpdateChannelParticipant | UpdateBotStopped | UpdateGroupCallConnection | UpdateBotCommands | UpdatePendingJoinRequests | UpdateBotChatInviteRequester | UpdateMessageReactions | UpdateAttachMenuBots | UpdateWebViewResultSent | UpdateBotMenuButton | UpdateSavedRingtones | UpdateTranscribedAudio | UpdateReadFeaturedEmojiStickers | UpdateUserEmojiStatus | UpdateRecentEmojiStatuses | UpdateRecentReactions | UpdateMoveStickerSetToTop | UpdateMessageExtendedMedia | UpdateChannelPinnedTopic | UpdateChannelPinnedTopics | UpdateUser | UpdateAutoSaveSettings | UpdateGroupInvitePrivacyForbidden | UpdateStory | UpdateReadStories | UpdateStoryID; + export type TypeUpdate = UpdateNewMessage | UpdateMessageID | UpdateDeleteMessages | UpdateUserTyping | UpdateChatUserTyping | UpdateChatParticipants | UpdateUserStatus | UpdateUserName | UpdateNewEncryptedMessage | UpdateEncryptedChatTyping | UpdateEncryption | UpdateEncryptedMessagesRead | UpdateChatParticipantAdd | UpdateChatParticipantDelete | UpdateDcOptions | UpdateNotifySettings | UpdateServiceNotification | UpdatePrivacy | UpdateUserPhone | UpdateReadHistoryInbox | UpdateReadHistoryOutbox | UpdateWebPage | UpdateReadMessagesContents | UpdateChannelTooLong | UpdateChannel | UpdateNewChannelMessage | UpdateReadChannelInbox | UpdateDeleteChannelMessages | UpdateChannelMessageViews | UpdateChatParticipantAdmin | UpdateNewStickerSet | UpdateStickerSetsOrder | UpdateStickerSets | UpdateSavedGifs | UpdateBotInlineQuery | UpdateBotInlineSend | UpdateEditChannelMessage | UpdateBotCallbackQuery | UpdateEditMessage | UpdateInlineBotCallbackQuery | UpdateReadChannelOutbox | UpdateDraftMessage | UpdateReadFeaturedStickers | UpdateRecentStickers | UpdateConfig | UpdatePtsChanged | UpdateChannelWebPage | UpdateDialogPinned | UpdatePinnedDialogs | UpdateBotWebhookJSON | UpdateBotWebhookJSONQuery | UpdateBotShippingQuery | UpdateBotPrecheckoutQuery | UpdatePhoneCall | UpdateLangPackTooLong | UpdateLangPack | UpdateFavedStickers | UpdateChannelReadMessagesContents | UpdateContactsReset | UpdateChannelAvailableMessages | UpdateDialogUnreadMark | UpdateMessagePoll | UpdateChatDefaultBannedRights | UpdateFolderPeers | UpdatePeerSettings | UpdatePeerLocated | UpdateNewScheduledMessage | UpdateDeleteScheduledMessages | UpdateTheme | UpdateGeoLiveViewed | UpdateLoginToken | UpdateMessagePollVote | UpdateDialogFilter | UpdateDialogFilterOrder | UpdateDialogFilters | UpdatePhoneCallSignalingData | UpdateChannelMessageForwards | UpdateReadChannelDiscussionInbox | UpdateReadChannelDiscussionOutbox | UpdatePeerBlocked | UpdateChannelUserTyping | UpdatePinnedMessages | UpdatePinnedChannelMessages | UpdateChat | UpdateGroupCallParticipants | UpdateGroupCall | UpdatePeerHistoryTTL | UpdateChatParticipant | UpdateChannelParticipant | UpdateBotStopped | UpdateGroupCallConnection | UpdateBotCommands | UpdatePendingJoinRequests | UpdateBotChatInviteRequester | UpdateMessageReactions | UpdateAttachMenuBots | UpdateWebViewResultSent | UpdateBotMenuButton | UpdateSavedRingtones | UpdateTranscribedAudio | UpdateReadFeaturedEmojiStickers | UpdateUserEmojiStatus | UpdateRecentEmojiStatuses | UpdateRecentReactions | UpdateMoveStickerSetToTop | UpdateMessageExtendedMedia | UpdateChannelPinnedTopic | UpdateChannelPinnedTopics | UpdateUser | UpdateAutoSaveSettings | UpdateGroupInvitePrivacyForbidden | UpdateStory | UpdateReadStories | UpdateStoryID | UpdateStoriesStealthMode | UpdateSentStoryReaction; export type TypeUpdates = UpdatesTooLong | UpdateShortMessage | UpdateShortChatMessage | UpdateShort | UpdatesCombined | Updates | UpdateShortSentMessage; export type TypeDcOption = DcOption; export type TypeConfig = Config; @@ -318,6 +318,9 @@ namespace Api { export type TypeStoryView = StoryView; export type TypeInputReplyTo = InputReplyToMessage | InputReplyToStory; export type TypeExportedStoryLink = ExportedStoryLink; + export type TypeStoriesStealthMode = StoriesStealthMode; + export type TypeMediaAreaCoordinates = MediaAreaCoordinates; + export type TypeMediaArea = MediaAreaVenue | InputMediaAreaVenue | MediaAreaGeoPoint; export type TypeResPQ = ResPQ; export type TypeP_Q_inner_data = PQInnerData | PQInnerDataDc | PQInnerDataTemp | PQInnerDataTempDc; export type TypeServer_DH_Params = ServerDHParamsFail | ServerDHParamsOk; @@ -2206,6 +2209,7 @@ namespace Api { voiceMessagesForbidden?: true; translationsDisabled?: true; storiesPinnedAvailable?: true; + blockedMyStoriesFrom?: true; id: long; about?: string; settings: Api.TypePeerSettings; @@ -2236,6 +2240,7 @@ namespace Api { voiceMessagesForbidden?: true; translationsDisabled?: true; storiesPinnedAvailable?: true; + blockedMyStoriesFrom?: true; id: long; about?: string; settings: Api.TypePeerSettings; @@ -2958,11 +2963,15 @@ namespace Api { readMaxId: int; }; export class UpdatePeerBlocked extends VirtualClass<{ + // flags: undefined; + blocked?: true; + blockedMyStoriesFrom?: true; peerId: Api.TypePeer; - blocked: Bool; }> { + // flags: undefined; + blocked?: true; + blockedMyStoriesFrom?: true; peerId: Api.TypePeer; - blocked: Bool; }; export class UpdateChannelUserTyping extends VirtualClass<{ // flags: undefined; @@ -3258,6 +3267,20 @@ namespace Api { id: int; randomId: long; }; + export class UpdateStoriesStealthMode extends VirtualClass<{ + stealthMode: Api.TypeStoriesStealthMode; + }> { + stealthMode: Api.TypeStoriesStealthMode; + }; + export class UpdateSentStoryReaction extends VirtualClass<{ + userId: long; + storyId: int; + reaction: Api.TypeReaction; + }> { + userId: long; + storyId: int; + reaction: Api.TypeReaction; + }; export class UpdatesTooLong extends VirtualClass {}; export class UpdateShortMessage extends VirtualClass<{ // flags: undefined; @@ -8289,10 +8312,12 @@ namespace Api { export class StoryViews extends VirtualClass<{ // flags: undefined; viewsCount: int; + reactionsCount: int; recentViewers?: long[]; }> { // flags: undefined; viewsCount: int; + reactionsCount: int; recentViewers?: long[]; }; export class StoryItemDeleted extends VirtualClass<{ @@ -8329,8 +8354,10 @@ namespace Api { caption?: string; entities?: Api.TypeMessageEntity[]; media: Api.TypeMessageMedia; + mediaAreas?: Api.TypeMediaArea[]; privacy?: Api.TypePrivacyRule[]; views?: Api.TypeStoryViews; + sentReaction?: Api.TypeReaction; }> { // flags: undefined; pinned?: true; @@ -8347,8 +8374,10 @@ namespace Api { caption?: string; entities?: Api.TypeMessageEntity[]; media: Api.TypeMessageMedia; + mediaAreas?: Api.TypeMediaArea[]; privacy?: Api.TypePrivacyRule[]; views?: Api.TypeStoryViews; + sentReaction?: Api.TypeReaction; }; export class UserStories extends VirtualClass<{ // flags: undefined; @@ -8362,11 +8391,19 @@ namespace Api { stories: Api.TypeStoryItem[]; }; export class StoryView extends VirtualClass<{ + // flags: undefined; + blocked?: true; + blockedMyStoriesFrom?: true; userId: long; date: int; + reaction?: Api.TypeReaction; }> { + // flags: undefined; + blocked?: true; + blockedMyStoriesFrom?: true; userId: long; date: int; + reaction?: Api.TypeReaction; }; export class InputReplyToMessage extends VirtualClass<{ // flags: undefined; @@ -8389,6 +8426,61 @@ namespace Api { }> { link: string; }; + export class StoriesStealthMode extends VirtualClass<{ + // flags: undefined; + activeUntilDate?: int; + cooldownUntilDate?: int; + } | void> { + // flags: undefined; + activeUntilDate?: int; + cooldownUntilDate?: int; + }; + export class MediaAreaCoordinates extends VirtualClass<{ + x: double; + y: double; + w: double; + h: double; + rotation: double; + }> { + x: double; + y: double; + w: double; + h: double; + rotation: double; + }; + export class MediaAreaVenue extends VirtualClass<{ + coordinates: Api.TypeMediaAreaCoordinates; + geo: Api.TypeGeoPoint; + title: string; + address: string; + provider: string; + venueId: string; + venueType: string; + }> { + coordinates: Api.TypeMediaAreaCoordinates; + geo: Api.TypeGeoPoint; + title: string; + address: string; + provider: string; + venueId: string; + venueType: string; + }; + export class InputMediaAreaVenue extends VirtualClass<{ + coordinates: Api.TypeMediaAreaCoordinates; + queryId: long; + resultId: string; + }> { + coordinates: Api.TypeMediaAreaCoordinates; + queryId: long; + resultId: string; + }; + export class MediaAreaGeoPoint extends VirtualClass<{ + coordinates: Api.TypeMediaAreaCoordinates; + geo: Api.TypeGeoPoint; + }> { + coordinates: Api.TypeMediaAreaCoordinates; + geo: Api.TypeGeoPoint; + }; export class ResPQ extends VirtualClass<{ nonce: int128; serverNonce: int128; @@ -10551,9 +10643,13 @@ namespace Api { export namespace stories { export class AllStoriesNotModified extends VirtualClass<{ + // flags: undefined; state: string; + stealthMode: Api.TypeStoriesStealthMode; }> { + // flags: undefined; state: string; + stealthMode: Api.TypeStoriesStealthMode; }; export class AllStories extends VirtualClass<{ // flags: undefined; @@ -10562,6 +10658,7 @@ namespace Api { state: string; userStories: Api.TypeUserStories[]; users: Api.TypeUser[]; + stealthMode: Api.TypeStoriesStealthMode; }> { // flags: undefined; hasMore?: true; @@ -10569,6 +10666,7 @@ namespace Api { state: string; userStories: Api.TypeUserStories[]; users: Api.TypeUser[]; + stealthMode: Api.TypeStoriesStealthMode; }; export class Stories extends VirtualClass<{ count: int; @@ -10587,13 +10685,19 @@ namespace Api { users: Api.TypeUser[]; }; export class StoryViewsList extends VirtualClass<{ + // flags: undefined; count: int; + reactionsCount: int; views: Api.TypeStoryView[]; users: Api.TypeUser[]; + nextOffset?: string; }> { + // flags: undefined; count: int; + reactionsCount: int; views: Api.TypeStoryView[]; users: Api.TypeUser[]; + nextOffset?: string; }; export class StoryViews extends VirtualClass<{ views: Api.TypeStoryViews[]; @@ -11507,19 +11611,31 @@ namespace Api { phones: string[]; }; export class Block extends Request, Bool> { + // flags: undefined; + myStoriesFrom?: true; id: Api.TypeInputPeer; }; export class Unblock extends Request, Bool> { + // flags: undefined; + myStoriesFrom?: true; id: Api.TypeInputPeer; }; export class GetBlocked extends Request, contacts.TypeBlocked> { + // flags: undefined; + myStoriesFrom?: true; offset: int; limit: int; }; @@ -11643,6 +11759,17 @@ namespace Api { id: Api.TypeInputUser; hidden: Bool; }; + export class SetBlocked extends Request, Bool> { + // flags: undefined; + myStoriesFrom?: true; + id: Api.TypeInputPeer[]; + limit: int; + }; } export namespace messages { @@ -14936,6 +15063,7 @@ namespace Api { pinned?: true; noforwards?: true; media: Api.TypeInputMedia; + mediaAreas?: Api.TypeMediaArea[]; caption?: string; entities?: Api.TypeMessageEntity[]; privacyRules: Api.TypeInputPrivacyRule[]; @@ -14946,6 +15074,7 @@ namespace Api { pinned?: true; noforwards?: true; media: Api.TypeInputMedia; + mediaAreas?: Api.TypeMediaArea[]; caption?: string; entities?: Api.TypeMessageEntity[]; privacyRules: Api.TypeInputPrivacyRule[]; @@ -14956,6 +15085,7 @@ namespace Api { // flags: undefined; id: int; media?: Api.TypeInputMedia; + mediaAreas?: Api.TypeMediaArea[]; caption?: string; entities?: Api.TypeMessageEntity[]; privacyRules?: Api.TypeInputPrivacyRule[]; @@ -14963,6 +15093,7 @@ namespace Api { // flags: undefined; id: int; media?: Api.TypeInputMedia; + mediaAreas?: Api.TypeMediaArea[]; caption?: string; entities?: Api.TypeMessageEntity[]; privacyRules?: Api.TypeInputPrivacyRule[]; @@ -15039,14 +15170,20 @@ namespace Api { id: int[]; }; export class GetStoryViewsList extends Request, stories.TypeStoryViewsList> { + // flags: undefined; + justContacts?: true; + reactionsFirst?: true; + q?: string; id: int; - offsetDate: int; - offsetId: long; + offset: string; limit: int; }; export class GetStoriesViews extends Request, Api.TypeUpdates> { + // flags: undefined; + past?: true; + future?: true; + }; + export class SendReaction extends Request, Api.TypeUpdates> { + // flags: undefined; + addToRecent?: true; + userId: Api.TypeInputUser; + storyId: int; + reaction: Api.TypeReaction; + }; } export type AnyRequest = InvokeAfterMsg | InvokeAfterMsgs | InitConnection | InvokeWithLayer | InvokeWithoutUpdates | InvokeWithMessagesRange | InvokeWithTakeout | ReqPq | ReqPqMulti | ReqPqMultiNew | ReqDHParams | SetClientDHParams | DestroyAuthKey | RpcDropAnswer | GetFutureSalts | Ping | PingDelayDisconnect | DestroySession | auth.SendCode | auth.SignUp | auth.SignIn | auth.LogOut | auth.ResetAuthorizations | auth.ExportAuthorization | auth.ImportAuthorization | auth.BindTempAuthKey | auth.ImportBotAuthorization | auth.CheckPassword | auth.RequestPasswordRecovery | auth.RecoverPassword | auth.ResendCode | auth.CancelCode | auth.DropTempAuthKeys | auth.ExportLoginToken | auth.ImportLoginToken | auth.AcceptLoginToken | auth.CheckRecoveryPassword | auth.ImportWebTokenAuthorization | auth.RequestFirebaseSms | auth.ResetLoginEmail | account.RegisterDevice | account.UnregisterDevice | account.UpdateNotifySettings | account.GetNotifySettings | account.ResetNotifySettings | account.UpdateProfile | account.UpdateStatus | account.GetWallPapers | account.ReportPeer | account.CheckUsername | account.UpdateUsername | account.GetPrivacy | account.SetPrivacy | account.DeleteAccount | account.GetAccountTTL | account.SetAccountTTL | account.SendChangePhoneCode | account.ChangePhone | account.UpdateDeviceLocked | account.GetAuthorizations | account.ResetAuthorization | account.GetPassword | account.GetPasswordSettings | account.UpdatePasswordSettings | account.SendConfirmPhoneCode | account.ConfirmPhone | account.GetTmpPassword | account.GetWebAuthorizations | account.ResetWebAuthorization | account.ResetWebAuthorizations | account.GetAllSecureValues | account.GetSecureValue | account.SaveSecureValue | account.DeleteSecureValue | account.GetAuthorizationForm | account.AcceptAuthorization | account.SendVerifyPhoneCode | account.VerifyPhone | account.SendVerifyEmailCode | account.VerifyEmail | account.InitTakeoutSession | account.FinishTakeoutSession | account.ConfirmPasswordEmail | account.ResendPasswordEmail | account.CancelPasswordEmail | account.GetContactSignUpNotification | account.SetContactSignUpNotification | account.GetNotifyExceptions | account.GetWallPaper | account.UploadWallPaper | account.SaveWallPaper | account.InstallWallPaper | account.ResetWallPapers | account.GetAutoDownloadSettings | account.SaveAutoDownloadSettings | account.UploadTheme | account.CreateTheme | account.UpdateTheme | account.SaveTheme | account.InstallTheme | account.GetTheme | account.GetThemes | account.SetContentSettings | account.GetContentSettings | account.GetMultiWallPapers | account.GetGlobalPrivacySettings | account.SetGlobalPrivacySettings | account.ReportProfilePhoto | account.ResetPassword | account.DeclinePasswordReset | account.GetChatThemes | account.SetAuthorizationTTL | account.ChangeAuthorizationSettings | account.GetSavedRingtones | account.SaveRingtone | account.UploadRingtone | account.UpdateEmojiStatus | account.GetDefaultEmojiStatuses | account.GetRecentEmojiStatuses | account.ClearRecentEmojiStatuses | account.ReorderUsernames | account.ToggleUsername | account.GetDefaultProfilePhotoEmojis | account.GetDefaultGroupPhotoEmojis | account.GetAutoSaveSettings | account.SaveAutoSaveSettings | account.DeleteAutoSaveExceptions | account.InvalidateSignInCodes | users.GetUsers | users.GetFullUser | users.SetSecureValueErrors | users.GetStoriesMaxIDs - | contacts.GetContactIDs | contacts.GetStatuses | contacts.GetContacts | contacts.ImportContacts | contacts.DeleteContacts | contacts.DeleteByPhones | contacts.Block | contacts.Unblock | contacts.GetBlocked | contacts.Search | contacts.ResolveUsername | contacts.GetTopPeers | contacts.ResetTopPeerRating | contacts.ResetSaved | contacts.GetSaved | contacts.ToggleTopPeers | contacts.AddContact | contacts.AcceptContact | contacts.GetLocated | contacts.BlockFromReplies | contacts.ResolvePhone | contacts.ExportContactToken | contacts.ImportContactToken | contacts.EditCloseFriends | contacts.ToggleStoriesHidden + | contacts.GetContactIDs | contacts.GetStatuses | contacts.GetContacts | contacts.ImportContacts | contacts.DeleteContacts | contacts.DeleteByPhones | contacts.Block | contacts.Unblock | contacts.GetBlocked | contacts.Search | contacts.ResolveUsername | contacts.GetTopPeers | contacts.ResetTopPeerRating | contacts.ResetSaved | contacts.GetSaved | contacts.ToggleTopPeers | contacts.AddContact | contacts.AcceptContact | contacts.GetLocated | contacts.BlockFromReplies | contacts.ResolvePhone | contacts.ExportContactToken | contacts.ImportContactToken | contacts.EditCloseFriends | contacts.ToggleStoriesHidden | contacts.SetBlocked | messages.GetMessages | messages.GetDialogs | messages.GetHistory | messages.Search | messages.ReadHistory | messages.DeleteHistory | messages.DeleteMessages | messages.ReceivedMessages | messages.SetTyping | messages.SendMessage | messages.SendMedia | messages.ForwardMessages | messages.ReportSpam | messages.GetPeerSettings | messages.Report | messages.GetChats | messages.GetFullChat | messages.EditChatTitle | messages.EditChatPhoto | messages.AddChatUser | messages.DeleteChatUser | messages.CreateChat | messages.GetDhConfig | messages.RequestEncryption | messages.AcceptEncryption | messages.DiscardEncryption | messages.SetEncryptedTyping | messages.ReadEncryptedHistory | messages.SendEncrypted | messages.SendEncryptedFile | messages.SendEncryptedService | messages.ReceivedQueue | messages.ReportEncryptedSpam | messages.ReadMessageContents | messages.GetStickers | messages.GetAllStickers | messages.GetWebPagePreview | messages.ExportChatInvite | messages.CheckChatInvite | messages.ImportChatInvite | messages.GetStickerSet | messages.InstallStickerSet | messages.UninstallStickerSet | messages.StartBot | messages.GetMessagesViews | messages.EditChatAdmin | messages.MigrateChat | messages.SearchGlobal | messages.ReorderStickerSets | messages.GetDocumentByHash | messages.GetSavedGifs | messages.SaveGif | messages.GetInlineBotResults | messages.SetInlineBotResults | messages.SendInlineBotResult | messages.GetMessageEditData | messages.EditMessage | messages.EditInlineBotMessage | messages.GetBotCallbackAnswer | messages.SetBotCallbackAnswer | messages.GetPeerDialogs | messages.SaveDraft | messages.GetAllDrafts | messages.GetFeaturedStickers | messages.ReadFeaturedStickers | messages.GetRecentStickers | messages.SaveRecentSticker | messages.ClearRecentStickers | messages.GetArchivedStickers | messages.GetMaskStickers | messages.GetAttachedStickers | messages.SetGameScore | messages.SetInlineGameScore | messages.GetGameHighScores | messages.GetInlineGameHighScores | messages.GetCommonChats | messages.GetWebPage | messages.ToggleDialogPin | messages.ReorderPinnedDialogs | messages.GetPinnedDialogs | messages.SetBotShippingResults | messages.SetBotPrecheckoutResults | messages.UploadMedia | messages.SendScreenshotNotification | messages.GetFavedStickers | messages.FaveSticker | messages.GetUnreadMentions | messages.ReadMentions | messages.GetRecentLocations | messages.SendMultiMedia | messages.UploadEncryptedFile | messages.SearchStickerSets | messages.GetSplitRanges | messages.MarkDialogUnread | messages.GetDialogUnreadMarks | messages.ClearAllDrafts | messages.UpdatePinnedMessage | messages.SendVote | messages.GetPollResults | messages.GetOnlines | messages.EditChatAbout | messages.EditChatDefaultBannedRights | messages.GetEmojiKeywords | messages.GetEmojiKeywordsDifference | messages.GetEmojiKeywordsLanguages | messages.GetEmojiURL | messages.GetSearchCounters | messages.RequestUrlAuth | messages.AcceptUrlAuth | messages.HidePeerSettingsBar | messages.GetScheduledHistory | messages.GetScheduledMessages | messages.SendScheduledMessages | messages.DeleteScheduledMessages | messages.GetPollVotes | messages.ToggleStickerSets | messages.GetDialogFilters | messages.GetSuggestedDialogFilters | messages.UpdateDialogFilter | messages.UpdateDialogFiltersOrder | messages.GetOldFeaturedStickers | messages.GetReplies | messages.GetDiscussionMessage | messages.ReadDiscussion | messages.UnpinAllMessages | messages.DeleteChat | messages.DeletePhoneCallHistory | messages.CheckHistoryImport | messages.InitHistoryImport | messages.UploadImportedMedia | messages.StartHistoryImport | messages.GetExportedChatInvites | messages.GetExportedChatInvite | messages.EditExportedChatInvite | messages.DeleteRevokedExportedChatInvites | messages.DeleteExportedChatInvite | messages.GetAdminsWithInvites | messages.GetChatInviteImporters | messages.SetHistoryTTL | messages.CheckHistoryImportPeer | messages.SetChatTheme | messages.GetMessageReadParticipants | messages.GetSearchResultsCalendar | messages.GetSearchResultsPositions | messages.HideChatJoinRequest | messages.HideAllChatJoinRequests | messages.ToggleNoForwards | messages.SaveDefaultSendAs | messages.SendReaction | messages.GetMessagesReactions | messages.GetMessageReactionsList | messages.SetChatAvailableReactions | messages.GetAvailableReactions | messages.SetDefaultReaction | messages.TranslateText | messages.GetUnreadReactions | messages.ReadReactions | messages.SearchSentMedia | messages.GetAttachMenuBots | messages.GetAttachMenuBot | messages.ToggleBotInAttachMenu | messages.RequestWebView | messages.ProlongWebView | messages.RequestSimpleWebView | messages.SendWebViewResultMessage | messages.SendWebViewData | messages.TranscribeAudio | messages.RateTranscribedAudio | messages.GetCustomEmojiDocuments | messages.GetEmojiStickers | messages.GetFeaturedEmojiStickers | messages.ReportReaction | messages.GetTopReactions | messages.GetRecentReactions | messages.ClearRecentReactions | messages.GetExtendedMedia | messages.SetDefaultHistoryTTL | messages.GetDefaultHistoryTTL | messages.SendBotRequestedPeer | messages.GetEmojiGroups | messages.GetEmojiStatusGroups | messages.GetEmojiProfilePhotoGroups | messages.SearchCustomEmoji | messages.TogglePeerTranslations | messages.GetBotApp | messages.RequestAppWebView | messages.SetChatWallPaper | updates.GetState | updates.GetDifference | updates.GetChannelDifference | photos.UpdateProfilePhoto | photos.UploadProfilePhoto | photos.DeletePhotos | photos.GetUserPhotos | photos.UploadContactProfilePhoto @@ -15093,6 +15252,6 @@ namespace Api { | folders.EditPeerFolders | stats.GetBroadcastStats | stats.LoadAsyncGraph | stats.GetMegagroupStats | stats.GetMessagePublicForwards | stats.GetMessageStats | chatlists.ExportChatlistInvite | chatlists.DeleteExportedInvite | chatlists.EditExportedInvite | chatlists.GetExportedInvites | chatlists.CheckChatlistInvite | chatlists.JoinChatlistInvite | chatlists.GetChatlistUpdates | chatlists.JoinChatlistUpdates | chatlists.HideChatlistUpdates | chatlists.GetLeaveChatlistSuggestions | chatlists.LeaveChatlist - | stories.SendStory | stories.EditStory | stories.DeleteStories | stories.TogglePinned | stories.GetAllStories | stories.GetUserStories | stories.GetPinnedStories | stories.GetStoriesArchive | stories.GetStoriesByID | stories.ToggleAllStoriesHidden | stories.GetAllReadUserStories | stories.ReadStories | stories.IncrementStoryViews | stories.GetStoryViewsList | stories.GetStoriesViews | stories.ExportStoryLink | stories.Report; + | stories.SendStory | stories.EditStory | stories.DeleteStories | stories.TogglePinned | stories.GetAllStories | stories.GetUserStories | stories.GetPinnedStories | stories.GetStoriesArchive | stories.GetStoriesByID | stories.ToggleAllStoriesHidden | stories.GetAllReadUserStories | stories.ReadStories | stories.IncrementStoryViews | stories.GetStoryViewsList | stories.GetStoriesViews | stories.ExportStoryLink | stories.Report | stories.ActivateStealthMode | stories.SendReaction; } diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index f4a33ef4a..a19d34052 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -181,7 +181,7 @@ inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; +userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; importedContact#c13e3c50 user_id:long client_id:long = ImportedContact; contactStatus#16d9703b user_id:long status:UserStatus = ContactStatus; @@ -297,7 +297,7 @@ updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update; updateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update; updateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update; updateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update; -updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; +updatePeerBlocked#ebe07752 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer = Update; updateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; @@ -332,6 +332,8 @@ updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; updateStory#205a4133 user_id:long story:StoryItem = Update; updateReadStories#feb5345a user_id:long max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; +updateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update; +updateSentStoryReaction#e3a73d20 user_id:long story_id:int reaction:Reaction = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference; updates.difference#f49ca0 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector state:updates.State = updates.Difference; @@ -1124,21 +1126,26 @@ messagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote; messagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote; messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = MessagePeerVote; sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#c64c0b97 flags:# views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; +storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; -stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories; -stories.allStories#839e0428 flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector = stories.AllStories; +stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; +stories.allStories#519d899e flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; stories.userStories#37a6ff5f stories:UserStories users:Vector = stories.UserStories; -storyView#a71aacc2 user_id:long date:int = StoryView; -stories.storyViewsList#fb3f77ac count:int views:Vector users:Vector = stories.StoryViewsList; +storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; +stories.storyViewsList#46e9b9ec flags:# count:int reactions_count:int views:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; inputReplyToMessage#9c5386e4 flags:# reply_to_msg_id:int top_msg_id:flags.0?int = InputReplyTo; inputReplyToStory#15b0f283 user_id:InputUser story_id:int = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; +storiesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode; +mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double = MediaAreaCoordinates; +mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; +inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; +mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X; @@ -1205,9 +1212,9 @@ users.getStoriesMaxIDs#ca1cb9ab id:Vector = Vector; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; contacts.deleteContacts#96a0e00 id:Vector = Updates; -contacts.block#68cc1411 id:InputPeer = Bool; -contacts.unblock#bea65d50 id:InputPeer = Bool; -contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked; +contacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; +contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; +contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; @@ -1437,7 +1444,7 @@ chatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite; chatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector = Updates; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; +stories.editStory#a9b91ae4 flags:# id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; @@ -1447,6 +1454,8 @@ stories.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories; stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector = stories.Stories; stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; -stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; +stories.getStoryViewsList#f95f61a4 flags:# just_contacts:flags.0?true reactions_first:flags.2?true q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; -stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool;`; \ No newline at end of file +stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool; +stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; +stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates;`; \ No newline at end of file diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 5f60c8158..76ed2d5d7 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -308,5 +308,7 @@ "stories.incrementStoryViews", "stories.getStoryViewsList", "stories.exportStoryLink", - "stories.report" + "stories.report", + "stories.activateStealthMode", + "stories.sendReaction" ] diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index 9c54c97bc..bced7ee92 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -221,7 +221,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; +userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -350,7 +350,7 @@ updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update; updateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update; updateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update; updateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update; -updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; +updatePeerBlocked#ebe07752 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer = Update; updateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; @@ -385,6 +385,8 @@ updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; updateStory#205a4133 user_id:long story:StoryItem = Update; updateReadStories#feb5345a user_id:long max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; +updateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update; +updateSentStoryReaction#e3a73d20 user_id:long story_id:int reaction:Reaction = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1526,24 +1528,24 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#c64c0b97 flags:# views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; +storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; -stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories; -stories.allStories#839e0428 flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector = stories.AllStories; +stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; +stories.allStories#519d899e flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; stories.userStories#37a6ff5f stories:UserStories users:Vector = stories.UserStories; -storyView#a71aacc2 user_id:long date:int = StoryView; +storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; -stories.storyViewsList#fb3f77ac count:int views:Vector users:Vector = stories.StoryViewsList; +stories.storyViewsList#46e9b9ec flags:# count:int reactions_count:int views:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; @@ -1552,6 +1554,14 @@ inputReplyToStory#15b0f283 user_id:InputUser story_id:int = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; +storiesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode; + +mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double = MediaAreaCoordinates; + +mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; +inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; +mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1685,9 +1695,9 @@ contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; contacts.deleteContacts#96a0e00 id:Vector = Updates; contacts.deleteByPhones#1013fd9e phones:Vector = Bool; -contacts.block#68cc1411 id:InputPeer = Bool; -contacts.unblock#bea65d50 id:InputPeer = Bool; -contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked; +contacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; +contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; +contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; @@ -1704,6 +1714,7 @@ contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; contacts.toggleStoriesHidden#753fb865 id:InputUser hidden:Bool = Bool; +contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -2088,8 +2099,8 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.sendStory#424cd47a flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; -stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; +stories.sendStory#d455fcec flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; +stories.editStory#a9b91ae4 flags:# id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; @@ -2101,7 +2112,9 @@ stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool; stories.getAllReadUserStories#729c562c = Updates; stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; -stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; +stories.getStoryViewsList#f95f61a4 flags:# just_contacts:flags.0?true reactions_first:flags.2?true q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool; +stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; +stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates; diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index b14a41369..abba2652a 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -7,6 +7,7 @@ import { animate } from '../../util/animation'; import cycleRestrict from '../../util/cycleRestrict'; import generateUniqueId from '../../util/generateUniqueId'; import launchMediaWorkers, { MAX_WORKERS } from '../../util/launchMediaWorkers'; +import Deferred from '../../util/Deferred'; interface Params { size: number; @@ -31,6 +32,8 @@ const LOW_PRIORITY_CACHE_MODULO = 0; const workers = launchMediaWorkers().map(({ connector }) => connector); const instancesByRenderId = new Map(); +const PENDING_CANVAS_RESIZES = new WeakMap>(); + let lastWorkerIndex = -1; class RLottie { @@ -213,15 +216,21 @@ class RLottie { this.params.noLoop = noLoop; } - setSharedCanvasCoords(viewId: string, newCoords: Params['coords']) { + async setSharedCanvasCoords(viewId: string, newCoords: Params['coords']) { const containerInfo = this.views.get(viewId)!; const { canvas, ctx, } = containerInfo; + const isCanvasDirty = !canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false'; + + if (!isCanvasDirty) { + await PENDING_CANVAS_RESIZES.get(canvas); + } + let [canvasWidth, canvasHeight] = [canvas.width, canvas.height]; - if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') { + if (isCanvasDirty) { const sizeFactor = this.calcSizeFactor(); ([canvasWidth, canvasHeight] = ensureCanvasSize(canvas, sizeFactor)); ctx.clearRect(0, 0, canvasWidth, canvasHeight); @@ -593,9 +602,12 @@ function ensureCanvasSize(canvas: HTMLCanvasElement, sizeFactor: number) { const expectedHeight = Math.round(canvas.offsetHeight * sizeFactor); if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) { + const deferred = new Deferred(); + PENDING_CANVAS_RESIZES.set(canvas, deferred.promise); requestMutation(() => { canvas.width = expectedWidth; canvas.height = expectedHeight; + deferred.resolve(); }); } diff --git a/src/styles/Telegram T.json b/src/styles/Telegram T.json index ae777d312..a9c0aadbf 100644 --- a/src/styles/Telegram T.json +++ b/src/styles/Telegram T.json @@ -2,73 +2,153 @@ "metadata": { "name": "Telegram T", "lastOpened": 0, - "created": 1690283628977 + "created": 1692722562593 }, "iconSets": [ { - "selection": [ - { - "order": 1074, - "name": "loop", - "prevSize": 32, - "id": 0, - "code": 59788, - "tempChar": "" - }, - { - "order": 1075, - "name": "skip-next", - "prevSize": 32, - "id": 1, - "code": 59789, - "tempChar": "" - }, - { - "order": 1076, - "name": "skip-previous", - "prevSize": 32, - "id": 2, - "code": 59790, - "tempChar": "" - }, - { - "order": 1077, - "name": "volume-1", - "prevSize": 32, - "id": 3, - "code": 59791, - "tempChar": "" - }, - { - "order": 1078, - "name": "volume-2", - "prevSize": 32, - "id": 4, - "code": 59792, - "tempChar": "" - }, - { - "order": 1079, - "name": "volume-3", - "prevSize": 32, - "id": 5, - "code": 59793, - "tempChar": "" - } - ], - "id": 3, - "metadata": { - "name": "Audio", - "importSize": { - "width": 24, - "height": 24 - } - }, - "height": 1024, - "prevSize": 32, "icons": [ { - "id": 0, + "id": 221, + "paths": [ + "M482.987 238.933c-128.043 8.619-246.869 75.093-337.237 188.715-19.371 24.363-25.557 34.347-30.677 49.536-4.053 12.117-4.437 15.317-3.755 31.403 0.981 22.144 4.48 31.36 21.333 56.448 100.907 150.144 252.331 232.064 407.040 220.288 99.328-7.552 184.448-45.696 264.789-118.699 43.136-39.168 95.573-106.581 104.789-134.613 6.827-20.821 4.736-47.531-5.291-67.499-9.472-18.901-40.363-56.875-71.040-87.339-99.285-98.645-221.44-146.901-349.952-138.24zM573.44 314.027c79.616 13.952 154.667 55.851 217.387 121.344 24.235 25.259 50.176 58.453 51.584 65.92 1.365 7.125-2.517 14.592-19.541 38.016-70.187 96.555-157.44 154.581-259.669 172.672-17.237 3.029-85.163 3.029-102.4 0-102.229-18.091-189.483-76.117-259.669-172.672-17.024-23.424-20.907-30.891-19.541-38.016 1.408-7.467 27.349-40.661 51.584-65.92 69.973-73.088 153.003-115.456 245.547-125.397 18.347-1.963 73.941 0.427 94.72 4.053zM481.28 341.589c-14.549 2.389-41.045 12.075-55.040 20.096-28.245 16.128-54.187 43.563-68.565 72.405-57.6 115.541 25.557 250.539 154.325 250.539 53.589 0 101.461-22.997 135.168-64.939 58.283-72.576 47.488-180.309-24.064-240.171-22.869-19.115-54.101-33.621-82.091-38.101-13.227-2.091-46.549-2.005-59.733 0.171zM530.645 411.477c29.781 4.949 59.52 27.776 73.173 56.149 13.867 28.757 13.867 59.904 0.043 88.747-9.045 18.859-28.629 38.869-46.464 47.488-28.715 13.867-62.080 13.867-90.795 0-17.835-8.619-37.419-28.629-46.464-47.488-19.328-40.277-11.648-84.651 20.011-116.139 25.301-25.088 55.253-34.645 90.496-28.757z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "eye-outline" + ] + }, + { + "id": 220, + "paths": [ + "M157.227 137.6c-5.632 2.816-11.52 7.637-15.659 12.885-10.496 13.312-11.051 24.533-1.792 37.76 2.773 3.925 160.427 162.389 350.336 352.085 240.811 240.555 347.307 345.813 351.957 347.947 14.379 6.485 32.512-1.707 43.264-19.584 6.997-11.648 7.125-18.56 0.555-29.867-3.115-5.333-28.203-31.659-67.243-70.656l-62.251-62.080 17.323-13.483c44.245-34.389 86.955-81.109 119.936-131.157 14.933-22.613 19.413-35.584 19.413-55.893-0.043-24.064-5.504-38.997-22.656-62.251-55.851-75.477-130.176-136.32-207.317-169.728-56.704-24.576-107.648-34.987-171.093-35.072-60.885-0.085-113.707 10.411-166.741 33.152l-16.213 6.997-69.376-69.291c-82.048-81.92-82.091-81.92-102.443-71.765zM548.693 310.656c90.496 10.027 172.971 52.523 242.133 124.715 24.235 25.259 50.176 58.453 51.584 65.92 1.365 7.125-2.517 14.592-19.541 38.016-28.245 38.869-65.067 77.013-98.901 102.4l-18.219 13.653-48.469-48.384 6.741-12.245c13.824-25.173 20.608-52.011 20.779-81.877 0.085-22.187-1.664-33.493-8.448-54.613-17.621-54.656-59.819-95.275-117.419-112.896-14.421-4.395-17.323-4.693-46.933-4.693-29.483 0-32.597 0.299-46.933 4.651-8.448 2.56-22.784 8.363-31.829 12.885l-16.512 8.277-16.683-16.768c-9.216-9.216-16.384-17.109-15.957-17.536 0.427-0.384 9.216-3.371 19.541-6.613 20.224-6.4 48.597-12.416 69.12-14.763 18.304-2.048 57.984-2.133 75.947-0.128zM220.843 349.312c-30.208 25.472-73.6 73.301-94.208 103.851-3.797 5.632-9.003 16.384-11.52 23.893-4.096 12.245-4.48 15.403-3.797 31.531 0.981 22.144 4.48 31.36 21.333 56.448 100.907 150.144 252.331 232.064 407.040 220.288 38.613-2.901 86.443-12.971 111.403-23.381l6.528-2.773-56.021-55.851-9.387 2.517c-25.728 6.784-39.851 8.277-79.36 8.363-41.899 0.043-51.2-0.981-83.712-9.344-86.827-22.229-165.205-79.147-228.011-165.547-17.024-23.424-20.907-30.891-19.541-38.016 2.347-12.501 53.248-71.083 84.181-96.896 8.235-6.869 16.128-13.525 17.493-14.763 2.176-1.92-0.939-5.717-22.144-26.965-13.611-13.611-25.173-24.747-25.728-24.747-0.597 0-7.125 5.12-14.549 11.392zM533.333 412.16c38.784 8.149 70.4 40.021 78.72 79.36 3.328 15.872 2.261 35.456-2.816 51.883l-3.669 11.819-136.32-136.235 5.589-2.304c16.427-6.827 39.083-8.576 58.496-4.523zM348.288 456.149c-9.771 25.771-12.032 64.469-5.419 93.397 15.019 65.749 65.835 116.565 131.584 131.584 28.928 6.613 67.627 4.352 93.397-5.419 4.48-1.707 4.181-2.091-25.216-31.531-27.52-27.563-30.208-29.781-35.883-29.781-9.685 0-31.531-6.229-43.093-12.288-15.573-8.149-34.901-28.075-42.709-44.032-6.101-12.501-11.264-31.488-11.307-41.813-0.043-4.395-4.523-9.643-29.824-34.901-29.44-29.397-29.824-29.696-31.531-25.216z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "eye-closed-outline" + ] + }, + { + "id": 219, + "paths": [ + "M256 136.619c-34.56 3.755-65.109 20.651-89.515 49.451-11.492 13.624-20.373 29.863-25.619 47.66l-0.237 0.938c-4.053 13.483-4.651 18.219-4.651 38.4s0.597 24.917 4.651 38.4c17.579 57.941 69.248 98.005 127.317 98.773 15.957 0.171 18.688-0.213 24.96-3.84 10.709-6.101 15.147-15.189 15.147-30.933 0-22.912-9.429-32.853-33.28-35.029-18.731-1.707-29.397-5.248-40.576-13.483-16.824-12.171-27.643-31.744-27.643-53.844 0-13.619 4.109-26.279 11.154-36.806l-0.151 0.24c8.064-12.117 22.187-23.339 33.963-26.88 2.944-0.896 12.032-2.304 20.224-3.115 26.624-2.645 36.309-12.203 36.309-35.883 0-28.032-15.317-38.059-52.053-34.048zM336.939 168.789c-16.512 7.509-27.435 23.125-25.899 36.992 0.819 5.286 2.713 10.012 5.456 14.122l-0.080-0.127c7.083 10.027 18.603 27.136 22.357 33.152 5.035 8.107 13.696 13.312 22.229 13.312 20.949 0 41.771-16.469 41.771-33.024 0-18.261-25.557-56.32-43.221-64.384-8.917-4.011-13.824-4.053-22.613-0.043zM502.613 238.763c-17.237 2.944-25.6 14.165-25.6 34.304 0 15.744 4.437 24.832 15.147 30.933l7.040 4.053h366.933l7.040-4.053c10.709-6.101 15.147-15.189 15.147-30.933s-4.437-24.832-15.147-30.933l-7.040-4.053-178.347-0.256c-98.091-0.128-181.419 0.299-185.173 0.939zM352.427 280.491c-6.656 3.072-11.648 8.661-19.371 21.419-3.413 5.717-9.216 14.549-12.843 19.627-6.827 9.685-9.429 20.224-7.168 29.227 1.579 6.357 15.872 21.205 24.021 24.917 13.568 6.187 24.149 3.413 37.248-9.813 9.429-9.515 22.016-28.843 25.856-39.723 5.035-14.379 2.816-27.179-6.187-35.371-10.965-9.984-31.36-15.019-41.557-10.283zM161.28 477.696c-17.195 2.901-25.6 14.165-25.6 34.304 0 15.744 4.437 24.832 15.147 30.933l7.040 4.053h708.267l7.040-4.053c10.709-6.101 15.147-15.189 15.147-30.933s-4.437-24.832-15.147-30.933l-7.040-4.053-349.013-0.213c-191.957-0.128-352.085 0.256-355.84 0.896zM161.28 716.629c-17.237 2.944-25.6 14.165-25.6 34.304 0 15.744 4.437 24.832 15.147 30.933l7.040 4.053h366.933l7.040-4.053c10.709-6.101 15.147-15.189 15.147-30.933s-4.437-24.832-15.147-30.933l-7.040-4.053-178.347-0.256c-98.091-0.128-181.419 0.299-185.173 0.939z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "story-caption" + ] + }, + { + "id": 218, + "paths": [ + "M315.691 179.243c-78.165 5.973-148.096 37.333-205.44 92.117-53.333 50.987-86.571 114.603-99.413 190.293-3.968 23.509-3.968 77.184 0 100.693 10.24 60.416 33.493 113.152 69.504 157.739 60.843 75.392 145.579 119.211 243.072 125.653 9.387 0.64 28.587 0.213 42.624-0.853 18.005-1.408 25.728-1.493 26.155-0.213 0.427 1.237 17.067 1.835 51.669 1.835h51.072v-71.595l-60.288-0.939-4.779-14.251-9.685 3.328c-32.512 11.179-84.224 14.891-120.661 8.661-60.203-10.283-112.597-38.784-152.832-83.072-84.267-92.757-91.051-233.045-16.128-333.696 63.659-85.589 173.739-124.075 277.973-97.152 7.381 1.92 13.611 3.243 13.867 2.987 0.213-0.299 1.109-2.816 2.048-5.632 1.493-4.693 2.347-5.163 10.453-5.632l8.832-0.512v-71.509l-55.893 0.256c-30.72 0.128-63.232 0.811-72.149 1.493zM512 213.333v35.84h102.357l-6.741 17.067 15.787 7.595c19.925 9.557 32.939 17.408 48.683 29.483 7.296 5.547 13.099 8.875 13.995 7.979 0.811-0.853 11.136-12.672 22.955-26.283l21.461-24.747-7.296-6.443c-9.643-8.576-33.024-24.533-49.92-34.048-16.128-9.131-38.485-18.859-39.936-17.451-0.555 0.555-5.035 11.136-9.899 23.509l-8.875 22.485-0.171-70.827h-102.4v35.84zM802.261 205.227c-0.853 2.091-4.992 12.672-9.173 23.467-4.224 10.795-9.301 23.68-11.264 28.587l-3.541 8.96 15.787 7.595c19.925 9.557 32.939 17.408 48.683 29.483 7.296 5.547 13.099 8.875 13.995 7.979 0.811-0.853 11.136-12.672 22.955-26.283l21.461-24.747-7.296-6.443c-9.643-8.576-33.024-24.533-49.92-34.048-11.477-6.485-37.035-18.389-39.467-18.389-0.384 0-1.408 1.749-2.219 3.84zM588.757 320.512c-8.021 6.272-20.053 15.701-26.752 20.907l-12.16 9.472 10.197 15.275c10.197 15.189 22.016 38.272 27.947 54.528 1.707 4.693 3.328 8.789 3.584 9.131 0.384 0.555 63.019-19.755 65.835-21.376 2.773-1.536-15.104-43.563-27.349-64.341-9.771-16.555-23.040-35.157-25.045-35.072-0.939 0-8.277 5.163-16.256 11.477zM346.453 341.632c-11.221 5.803-101.973 79.147-105.899 85.547-9.728 15.957-2.603 36.821 16.683 49.067 14.379 9.088 24.192 6.357 49.109-13.696l15.36-12.331 1.707 211.115 4.053 7.040c6.101 10.709 15.189 15.147 30.933 15.147s24.832-4.437 30.933-15.147l4.053-7.040 0.469-143.872c0.299-93.952-0.128-146.176-1.28-150.357-5.931-22.101-28.544-34.56-46.123-25.472zM804.693 435.157c-13.141 3.541-27.093 7.296-30.976 8.363l-7.040 1.963 1.536 6.357c3.712 15.531 6.571 42.197 6.571 61.952l0.043 21.419 34.389 2.944c18.901 1.579 34.859 2.432 35.413 1.877s0.981-13.824 0.896-29.44c-0.128-28.544-1.707-44.416-6.955-68.992-2.432-11.563-3.243-13.184-6.4-13.056-1.963 0.128-14.336 3.072-27.477 6.613zM975.36 435.157c-13.141 3.541-27.093 7.296-30.976 8.363l-7.040 1.963 1.536 6.357c3.712 15.531 6.571 42.197 6.571 61.952l0.043 21.419 34.389 2.944c18.901 1.579 34.859 2.432 35.413 1.877s0.981-13.824 0.896-29.44c-0.128-28.544-1.707-44.416-6.955-68.992-2.432-11.563-3.243-13.184-6.4-13.056-1.963 0.128-14.336 3.072-27.477 6.613zM593.408 588.373c-0.427 0.725-2.091 5.504-3.755 10.667-4.139 12.885-18.091 41.387-27.221 55.637-4.181 6.485-7.040 12.288-6.4 12.928s13.44 10.069 28.416 20.907l27.221 19.755 10.581-16.171c13.099-20.011 25.771-45.781 33.707-68.651l6.059-17.365-5.461-1.707c-15.403-4.864-58.965-17.28-60.587-17.28-1.024 0-2.176 0.555-2.56 1.28zM699.691 695.765c-13.909 14.037-24.96 23.253-46.891 39.040l-4.864 3.499 17.621 29.355c9.643 16.128 18.261 29.355 19.157 29.355 2.176 0 20.224-12.288 36.437-24.832 12.715-9.856 48.683-45.696 48.469-48.256-0.085-0.981-37.291-31.829-50.048-41.557-3.669-2.816-4.053-2.517-19.883 13.397zM870.357 695.765c-13.909 14.037-24.96 23.253-46.891 39.040l-4.864 3.499 17.621 29.355c9.643 16.128 18.261 29.355 19.157 29.355 2.176 0 20.224-12.288 36.437-24.832 12.715-9.856 48.683-45.696 48.469-48.256-0.085-0.981-37.291-31.829-50.048-41.557-3.669-2.816-4.053-2.517-19.883 13.397zM563.2 810.667v35.84h102.4v-71.68h-102.4v35.84z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "story-priority" + ] + }, + { + "id": 216, + "paths": [ + "M601.6 102.338c0-17.95-20.207-28.47-34.91-18.176l-109.79 76.854c-12.621 8.833-12.621 27.52 0 36.352l109.79 76.854c14.703 10.294 34.91-0.226 34.91-18.176v-42.329c136.094 31.019 237.653 152.786 237.653 298.275 0 168.956-136.964 305.92-305.92 305.92s-305.92-136.964-305.92-305.92c0-105.417 53.29-198.4 134.563-253.466 16.192-10.97 20.425-32.989 9.454-49.181s-32.99-20.425-49.181-9.454c-99.893 67.68-165.663 182.204-165.663 312.101 0 208.073 168.675 376.747 376.747 376.747 208.073 0 376.747-168.674 376.747-376.747 0-184.761-132.996-338.458-308.48-370.577v-39.076z", + "M544.798 645.867c-41.984 0-78.652-19.618-91.849-47.667-2.752-5.867-4.403-11.55-4.403-18.15 0-14.114 8.802-22.549 23.1-22.549 11.004 0 17.967 3.849 24.201 13.751 9.536 19.063 25.118 30.613 48.951 30.613 28.783 0 49.502-20.715 49.502-49.314 0-27.866-20.535-47.667-49.318-47.667-15.765 0-28.966 6.784-38.315 15.765-10.82 10.086-16.32 12.834-28.604 12.834-18.15 0-26.765-12.651-26.031-27.499 0-0.734 0-1.284 0.183-2.202l7.697-96.983c1.835-22.366 13.385-31.35 34.837-31.35h116.049c13.747 0 22.733 8.617 22.733 22s-8.802 22-22.733 22h-104.499l-6.234 73.516h1.097c10.82-17.050 32.452-27.682 59.401-27.682 50.603 0 86.537 35.567 86.537 85.982 0 56.465-41.984 94.601-102.302 94.601z" + ], + "attrs": [ + {}, + {} + ], + "width": 1067, + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "stealth-past" + ] + }, + { + "id": 215, + "paths": [ + "M443.733 102.338c0-17.95 20.207-28.47 34.91-18.176l109.79 76.854c12.621 8.833 12.621 27.52 0 36.352l-109.79 76.854c-14.703 10.294-34.91-0.226-34.91-18.176v-42.329c-136.093 31.019-237.653 152.786-237.653 298.275 0 168.956 136.965 305.92 305.92 305.92s305.92-136.964 305.92-305.92c0-105.417-53.291-198.4-134.562-253.466-16.192-10.97-20.425-32.989-9.455-49.181s32.99-20.425 49.182-9.454c99.891 67.68 165.662 182.204 165.662 312.101 0 208.073-168.674 376.747-376.747 376.747-208.071 0-376.747-168.674-376.747-376.747 0-184.761 132.998-338.458 308.48-370.577v-39.076z", + "M329.83 640c-17.417 0-26.033-9.715-26.033-23.65 0-10.449 4.033-17.417 15.217-27.682l78.833-75.349c32.635-31.351 41.066-44.186 41.066-63.253 0-21.815-16.866-37.399-40.516-37.399-21.267 0-35.933 10.633-45.467 31.899-6.233 10.999-12.65 16.137-24.567 16.137-14.85 0-23.65-8.802-23.65-22.554 0-4.215 0.733-8.064 2.017-11.915 8.983-29.333 42.167-56.65 92.217-56.65 56.098 0 93.867 31.533 93.867 77 0 32.265-15.765 52.433-59.034 93.316l-57.2 55.002v1.097h99.734c14.298 0 22.916 8.619 22.916 22.003 0 13.018-8.619 21.999-22.916 21.999h-146.484zM624.137 645.867c-41.984 0-78.647-19.618-91.849-47.667-2.752-5.867-4.399-11.55-4.399-18.15 0-14.114 8.798-22.549 23.1-22.549 10.999 0 17.967 3.849 24.201 13.751 9.532 19.063 25.114 30.613 48.947 30.613 28.783 0 49.502-20.715 49.502-49.314 0-27.866-20.535-47.667-49.318-47.667-15.765 0-28.966 6.784-38.315 15.765-10.816 10.086-16.32 12.834-28.599 12.834-18.15 0-26.769-12.651-26.035-27.499 0-0.734 0-1.284 0.183-2.202l7.701-96.983c1.83-22.366 13.38-31.35 34.833-31.35h116.049c13.751 0 22.733 8.617 22.733 22s-8.798 22-22.733 22h-104.499l-6.234 73.516h1.101c10.816-17.050 32.448-27.682 59.401-27.682 50.598 0 86.532 35.567 86.532 85.982 0 56.465-41.984 94.601-102.302 94.601z" + ], + "attrs": [ + {}, + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "stealth-future" + ] + }, + { + "id": 214, + "paths": [ + "M547.43 85.332v51.563c77.453 7.221 148.1 37.888 204.774 84.835l33.438-33.439c13.828-13.83 36.254-13.83 50.082 0s13.828 36.253 0 50.082l-33.438 33.439c54.012 65.202 86.477 148.903 86.477 240.187 0 208.073-168.674 376.747-376.747 376.747-208.071 0-376.746-168.674-376.746-376.747 0-196.13 149.868-357.255 341.333-375.105v-51.563c0-19.558 15.855-35.413 35.413-35.413s35.413 15.855 35.413 35.413zM512.017 206.079c-168.954 0-305.92 136.965-305.92 305.921s136.965 305.92 305.92 305.92c168.956 0 305.92-136.964 305.92-305.92s-136.964-305.921-305.92-305.921zM512.017 263.252c19.558 0 35.413 15.855 35.413 35.413v160.026c17.22 11.473 28.57 31.066 28.57 53.308 0 35.345-28.655 64-64 64s-64-28.655-64-64c0-22.259 11.362-41.865 28.604-53.329v-160.005c0-19.558 15.855-35.413 35.413-35.413z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "timer" + ] + }, + { + "id": 209, + "paths": [ + "M512 206.078c-168.955 0-305.92 136.965-305.92 305.922 0 168.951 136.965 305.92 305.92 305.92s305.92-136.969 305.92-305.92c0-168.957-136.964-305.922-305.92-305.922zM135.253 512c0-208.073 168.675-376.749 376.747-376.749 208.073 0 376.747 168.675 376.747 376.749 0 208.068-168.674 376.747-376.747 376.747-208.071 0-376.747-168.678-376.747-376.747zM512 322.987c19.558 0 35.413 15.855 35.413 35.413v221.705l76.079-76.079c13.828-13.828 36.254-13.828 50.082 0s13.828 36.254 0 50.082l-136.533 136.533c-13.828 13.828-36.254 13.828-50.082 0l-136.533-136.533c-13.83-13.828-13.83-36.254 0-50.082s36.252-13.828 50.082 0l76.079 76.079v-221.705c0-19.558 15.855-35.413 35.413-35.413z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "arrow-down-circle" + ] + }, + { + "id": 208, + "paths": [ + "M512 140.8c-3.203 0-6.397 0.040-9.581 0.121-94.87 2.403-180.895 40.401-245.218 101.138-4.754 4.489-9.39 9.102-13.901 13.834-61.020 64.002-99.407 149.773-102.321 244.476-0.119 3.862-0.179 7.739-0.179 11.63 0 3.205 0.040 6.395 0.121 9.58 2.309 91.136 37.464 174.106 94.068 237.517 8.653 9.697 17.808 18.934 27.424 27.674 63.204 57.446 146.308 93.379 237.75 96.246 3.931 0.123 7.877 0.184 11.837 0.184 16.466 0 32.666-1.070 48.532-3.139 21.028-2.744 40.3 12.078 43.044 33.111 2.744 21.028-12.083 40.3-33.111 43.044-19.154 2.499-38.666 3.784-58.465 3.784-5.799 0-11.572-0.113-17.318-0.328-108.223-4.116-206.614-46.618-281.945-114.284-11.321-10.168-22.121-20.905-32.355-32.164-72.31-79.56-116.382-185.247-116.382-301.225 0-119.364 46.681-227.824 122.783-308.122 5.237-5.526 10.613-10.918 16.123-16.172 80.393-76.648 189.247-123.707 309.093-123.707 19.799 0 39.311 1.287 58.465 3.785 21.028 2.743 35.855 22.014 33.111 43.044s-22.016 35.853-43.044 33.111c-15.867-2.070-32.067-3.14-48.532-3.14zM730.911 163.625c12.923-16.816 37.028-19.973 53.847-7.050 31.022 23.839 58.829 51.647 82.668 82.669 12.923 16.816 9.764 40.924-7.050 53.847s-40.924 9.766-53.847-7.050c-19.773-25.729-42.839-48.797-68.567-68.569-16.819-12.922-19.973-37.030-7.050-53.846zM913.172 420.425c21.028-2.743 40.3 12.081 43.044 33.111 2.499 19.151 3.784 38.667 3.784 58.464 0 19.799-1.285 39.311-3.784 58.465-2.744 21.028-22.016 35.855-43.044 33.111-21.033-2.744-35.855-22.016-33.111-43.044 2.068-15.867 3.139-32.067 3.139-48.532s-1.070-32.664-3.139-48.531c-2.744-21.029 12.078-40.301 33.111-43.044zM860.375 730.911c16.814 12.923 19.973 37.028 7.050 53.847-23.839 31.022-51.645 58.829-82.668 82.668-16.819 12.923-40.924 9.764-53.847-7.050s-9.769-40.924 7.050-53.847c25.728-19.773 48.794-42.839 68.567-68.567 12.923-16.819 37.033-19.973 53.847-7.050zM487.598 349.627v44.613c175.617 0 235.162 126.469 255.135 221.025h0.005c6.497 30.776 9.748 46.162 6.82 51.251-2.79 4.854-6.574 7.235-12.155 7.66-5.857 0.44-20.91-11.121-51.021-34.248-41.4-31.79-105.088-61.368-198.785-61.368v44.611c0 29.588 0 44.385-5.939 51.507-5.157 6.185-12.923 9.585-20.965 9.17-9.261-0.471-20.126-10.511-41.856-30.592l-147.996-136.776c-11.303-10.445-16.955-15.668-19.050-21.787-1.841-5.375-1.841-11.211 0-16.587 2.095-6.118 7.747-11.341 19.050-21.788l147.996-136.773c21.73-20.082 32.595-30.124 41.856-30.596 8.042-0.41 15.809 2.988 20.965 9.172 5.939 7.122 5.939 21.916 5.939 51.505z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "story-reply" + ] + }, + { + "id": 202, "paths": [ "M298.667 298.667h426.667v128l170.667-170.667-170.667-170.667v128h-512v256h85.333v-170.667zM725.333 725.333h-426.667v-128l-170.667 170.667 170.667 170.667v-128h512v-256h-85.333v170.667z" ], @@ -78,11 +158,11 @@ "tags": [ "loop" ], - "defaultCode": 59777, + "defaultCode": 59788, "grid": 24 }, { - "id": 1, + "id": 203, "paths": [ "M256 768l362.667-256-362.667-256v512zM682.667 256v512h85.333v-512h-85.333z" ], @@ -92,11 +172,11 @@ "tags": [ "skip-next" ], - "defaultCode": 59778, + "defaultCode": 59789, "grid": 24 }, { - "id": 2, + "id": 204, "paths": [ "M256 256h85.333v512h-85.333zM405.333 512l362.667 256v-512z" ], @@ -106,11 +186,11 @@ "tags": [ "skip-previous" ], - "defaultCode": 59779, + "defaultCode": 59790, "grid": 24 }, { - "id": 3, + "id": 205, "paths": [ "M558.623 229.209c5.383 8.831 8.569 19.512 8.569 30.939v509.203c0 33.212-26.923 60.135-60.135 60.135-11.427 0-22.108-3.187-31.207-8.72l0.267 0.149-160.939-96.567c-29.352-17.832-48.66-49.631-48.66-85.94v-247.323c0-36.309 19.309-68.108 48.217-85.692l0.443-0.251 160.939-96.565c28.48-17.087 65.419-7.853 82.505 20.627v0.005zM146.653 334.345c33.212 0 60.135 26.923 60.135 60.135v240.54c0 33.212-26.923 60.135-60.135 60.135s-60.135-26.923-60.135-60.135v-240.54c0-33.212 26.923-60.135 60.135-60.135v0zM554.485 623.967h-0.040c-3.997 0.005-7.956-0.781-11.648-2.313s-7.045-3.78-9.864-6.613l-0.001-0.001c-11.909-11.909-11.909-187.531 0-199.44 11.911-11.911 31.219-11.911 43.107 0 26.637 26.636 41.295 62.059 41.295 99.697 0 37.688-14.657 73.084-41.293 99.72-2.823 2.841-6.181 5.095-9.88 6.631-3.699 1.537-7.665 2.325-11.671 2.32h-0.004z" ], @@ -120,11 +200,11 @@ "tags": [ "volume-1" ], - "defaultCode": 59780, + "defaultCode": 59791, "grid": 24 }, { - "id": 4, + "id": 206, "paths": [ "M558.623 229.209c5.383 8.831 8.569 19.512 8.569 30.939v509.203c0 33.212-26.923 60.135-60.135 60.135-11.427 0-22.108-3.187-31.207-8.72l0.267 0.149-160.939-96.567c-29.352-17.832-48.66-49.631-48.66-85.94v-247.323c0-36.309 19.309-68.108 48.217-85.692l0.443-0.251 160.939-96.565c28.48-17.087 65.419-7.853 82.505 20.627v0.005zM146.653 334.345c33.212 0 60.135 26.923 60.135 60.135v240.54c0 33.212-26.923 60.135-60.135 60.135s-60.135-26.923-60.135-60.135v-240.54c0-33.212 26.923-60.135 60.135-60.135zM554.485 623.967h-0.040c-0.012 0-0.026 0-0.040 0-8.391 0-15.985-3.413-21.47-8.925l-0.001-0.001-0.001-0.001c-11.909-11.909-11.909-187.531 0-199.44 11.911-11.911 31.219-11.911 43.107 0 26.637 26.636 41.295 62.059 41.295 99.697 0 37.688-14.657 73.084-41.293 99.72-5.493 5.529-13.101 8.951-21.508 8.951-0.015 0-0.030-0-0.045-0l0.002 0h-0.004zM659.088 707.369h-0.024c-0.012 0-0.027 0-0.041 0-9.339 0-17.792-3.794-23.902-9.926l-0.001-0.001c-13.221-13.245-13.221-34.696 0-47.941 74.168-74.14 74.139-194.815 0-268.96-13.247-13.245-13.247-34.695 0-47.94 13.221-13.247 34.695-13.247 47.941 0 100.605 100.607 100.605 264.259 0.024 364.812-6.663 6.637-15.315 9.956-23.997 9.956z" ], @@ -134,11 +214,11 @@ "tags": [ "volume-2" ], - "defaultCode": 59781, + "defaultCode": 59792, "grid": 24 }, { - "id": 5, + "id": 207, "paths": [ "M558.623 229.209c5.383 8.831 8.569 19.512 8.569 30.939v509.203c0 33.212-26.923 60.135-60.135 60.135-11.427 0-22.108-3.187-31.207-8.72l0.267 0.149-160.939-96.567c-29.352-17.832-48.66-49.631-48.66-85.94v-247.323c0-36.309 19.309-68.108 48.217-85.692l0.443-0.251 160.939-96.565c28.48-17.087 65.419-7.853 82.505 20.627v0.005zM146.653 334.345c33.212 0 60.135 26.923 60.135 60.135v240.54c0 33.212-26.923 60.135-60.135 60.135s-60.135-26.923-60.135-60.135v-240.54c0-33.212 26.923-60.135 60.135-60.135zM554.485 623.967h-0.040c-0.012 0-0.026 0-0.040 0-8.391 0-15.985-3.413-21.47-8.925l-0.001-0.001-0.001-0.001c-11.909-11.909-11.909-187.531 0-199.44 11.911-11.911 31.219-11.911 43.107 0 26.637 26.636 41.295 62.059 41.295 99.697 0 37.688-14.657 73.084-41.293 99.72-5.493 5.529-13.101 8.951-21.508 8.951-0.015 0-0.030-0-0.045-0l0.002 0h-0.004zM659.088 707.369h-0.024c-0.012 0-0.027 0-0.041 0-9.339 0-17.792-3.794-23.902-9.926l-0.001-0.001c-13.221-13.245-13.221-34.696 0-47.941 74.168-74.14 74.139-194.815 0-268.96-13.247-13.245-13.247-34.695 0-47.94 13.221-13.247 34.695-13.247 47.941 0 100.605 100.607 100.605 264.259 0.024 364.812-6.663 6.637-15.315 9.956-23.997 9.956zM740.708 792.089c-13.341-13.339-13.341-34.941 0-48.283 61.275-61.275 95.013-142.712 95.013-229.413s-33.769-168.164-95.067-229.439c-13.317-13.34-13.317-34.943 0-48.283 13.368-13.34 34.967-13.34 48.307 0 74.187 74.187 115.039 172.789 115.039 277.721s-40.821 203.533-115.013 277.667c-6.167 6.195-14.699 10.029-24.128 10.029h-0.037c-8.715 0-17.429-3.344-24.113-10z" ], @@ -148,15 +228,9 @@ "tags": [ "volume-3" ], - "defaultCode": 59782, + "defaultCode": 59793, "grid": 24 - } - ], - "colorThemes": [], - "colorThemeIdx": 0 - }, - { - "icons": [ + }, { "id": 105, "paths": [ @@ -3462,8 +3536,8 @@ "metadata": { "name": "icons", "importSize": { - "width": 26, - "height": 19 + "width": 24, + "height": 24 } }, "preferences": { @@ -3505,1604 +3579,1724 @@ }, "selection": [ { - "order": 878, + "order": 1309, + "id": 187, + "name": "eye-outline", + "prevSize": 32, + "code": 59855, + "tempChar": "" + }, + { + "order": 1308, + "id": 186, + "name": "eye-closed-outline", + "prevSize": 32, + "code": 59859, + "tempChar": "" + }, + { + "order": 1307, + "id": 185, + "name": "story-caption", + "prevSize": 32, + "code": 59860, + "tempChar": "" + }, + { + "order": 1306, + "id": 184, + "name": "story-priority", + "prevSize": 32, + "code": 59854, + "tempChar": "" + }, + { + "order": 1302, + "id": 182, + "name": "stealth-past", + "prevSize": 32, + "code": 59856, + "tempChar": "" + }, + { + "order": 1301, + "id": 181, + "name": "stealth-future", + "prevSize": 32, + "code": 59857, + "tempChar": "" + }, + { + "order": 1303, + "id": 180, + "name": "timer", + "prevSize": 32, + "code": 59858, + "tempChar": "" + }, + { + "order": 1298, + "id": 175, + "name": "arrow-down-circle", + "prevSize": 32, + "code": 59861, + "tempChar": "" + }, + { + "order": 1297, + "id": 174, + "name": "story-reply", + "prevSize": 32, + "code": 59862, + "tempChar": "" + }, + { + "order": 1091, + "name": "loop", + "prevSize": 32, + "id": 168, + "code": 59788, + "tempChar": "" + }, + { + "order": 1092, + "name": "skip-next", + "prevSize": 32, + "id": 169, + "code": 59789, + "tempChar": "" + }, + { + "order": 1093, + "name": "skip-previous", + "prevSize": 32, + "id": 170, + "code": 59790, + "tempChar": "" + }, + { + "order": 1094, + "name": "volume-1", + "prevSize": 32, + "id": 171, + "code": 59791, + "tempChar": "" + }, + { + "order": 1095, + "name": "volume-2", + "prevSize": 32, + "id": 172, + "code": 59792, + "tempChar": "" + }, + { + "order": 1096, + "name": "volume-3", + "prevSize": 32, + "id": 173, + "code": 59793, + "tempChar": "" + }, + { + "order": 1097, "id": 71, "name": "hand-stop", "prevSize": 32, "code": 59847, - "tempChar": "" + "tempChar": "" }, { - "order": 879, + "order": 1098, "id": 72, "name": "more-circle", "prevSize": 32, "code": 59848, - "tempChar": "" + "tempChar": "" }, { - "order": 880, + "order": 1099, "id": 73, "name": "close-circle", "prevSize": 32, "code": 59849, - "tempChar": "" + "tempChar": "" }, { - "order": 881, + "order": 1100, "id": 74, "name": "play-story", "prevSize": 32, "code": 59846, - "tempChar": "" + "tempChar": "" }, { - "order": 882, + "order": 1101, "id": 75, "name": "story-expired", "prevSize": 32, "code": 59845, - "tempChar": "" + "tempChar": "" }, { - "order": 883, + "order": 1102, "id": 76, "name": "save-story", "prevSize": 32, "code": 59843, - "tempChar": "" + "tempChar": "" }, { - "order": 884, + "order": 1103, "id": 77, "name": "settings-filled", "prevSize": 32, "code": 59841, - "tempChar": "" + "tempChar": "" }, { - "order": 885, + "order": 1104, "id": 78, "name": "share-screen-stop", "prevSize": 32, "code": 59842, - "tempChar": "" + "tempChar": "" }, { - "order": 886, + "order": 1105, "id": 79, "name": "user-online", "prevSize": 32, "code": 59840, - "tempChar": "" + "tempChar": "" }, { - "order": 894, + "order": 1106, "id": 87, "name": "forums", "prevSize": 32, "code": 59828, - "tempChar": "" + "tempChar": "" }, { - "order": 895, + "order": 1107, "id": 88, "name": "hashtag", "prevSize": 32, "code": 59825, - "tempChar": "" + "tempChar": "" }, { - "order": 896, + "order": 1108, "id": 89, "name": "reopen-topic", "prevSize": 32, "code": 59826, - "tempChar": "" + "tempChar": "" }, { - "order": 897, + "order": 1109, "id": 90, "name": "close-topic", "prevSize": 32, "code": 59827, - "tempChar": "" + "tempChar": "" }, { - "order": 898, + "order": 1110, "id": 91, "name": "open-in-new-tab", "prevSize": 32, "code": 59823, - "tempChar": "" + "tempChar": "" }, { - "order": 899, + "order": 1111, "id": 92, "name": "pip", "prevSize": 32, "code": 59822, - "tempChar": "" + "tempChar": "" }, { - "order": 900, + "order": 1112, "id": 93, "name": "gift", "prevSize": 32, "code": 59821, - "tempChar": "" + "tempChar": "" }, { - "order": 901, + "order": 1113, "id": 94, "name": "sort", "prevSize": 32, "code": 59820, - "tempChar": "" + "tempChar": "" }, { - "order": 902, + "order": 1114, "id": 95, "name": "web", "prevSize": 32, "code": 59819, - "tempChar": "" + "tempChar": "" }, { - "order": 903, + "order": 1115, "id": 96, "name": "transcribe", "prevSize": 32, "code": 59818, - "tempChar": "" + "tempChar": "" }, { - "order": 904, + "order": 1116, "id": 97, "name": "add-one-badge", "prevSize": 32, "code": 59803, - "tempChar": "" + "tempChar": "" }, { - "order": 907, + "order": 1117, "id": 100, "name": "double-badge", "prevSize": 32, "code": 59810, - "tempChar": "" + "tempChar": "" }, { - "order": 908, + "order": 1118, "id": 101, "name": "file-badge", "prevSize": 32, "code": 59811, - "tempChar": "" + "tempChar": "" }, { - "order": 909, + "order": 1119, "id": 102, "name": "folder-badge", "prevSize": 32, "code": 59812, - "tempChar": "" + "tempChar": "" }, { - "order": 910, + "order": 1120, "id": 103, "name": "link-badge", "prevSize": 32, "code": 59813, - "tempChar": "" + "tempChar": "" }, { - "order": 912, + "order": 1121, "id": 105, "name": "premium", "prevSize": 32, "code": 59815, - "tempChar": "" + "tempChar": "" }, { - "order": 916, + "order": 1122, "id": 109, "name": "heart-outline", "prevSize": 32, "code": 59806, - "tempChar": "" + "tempChar": "" }, { - "order": 917, + "order": 1123, "id": 110, "name": "heart", "prevSize": 32, "code": 59807, - "tempChar": "" + "tempChar": "" }, { - "order": 918, + "order": 1124, "id": 111, "name": "word-wrap", "prevSize": 32, "code": 59805, - "tempChar": "" + "tempChar": "" }, { - "order": 919, + "order": 1125, "id": 112, "name": "webapp", "prevSize": 32, "code": 59795, - "tempChar": "" + "tempChar": "" }, { - "order": 920, + "order": 1126, "id": 113, "name": "reload", "prevSize": 32, "code": 59796, - "tempChar": "" + "tempChar": "" }, { - "order": 921, + "order": 1127, "id": 114, "name": "install", "prevSize": 32, "code": 59801, - "tempChar": "" + "tempChar": "" }, { - "order": 922, + "order": 1128, "id": 115, "name": "favorite-filled", "prevSize": 32, "code": 59800, - "tempChar": "" + "tempChar": "" }, { - "order": 924, + "order": 1129, "id": 117, "name": "video-outlined", "prevSize": 32, "code": 59799, - "tempChar": "" + "tempChar": "" }, { - "order": 925, + "order": 1130, "id": 118, "name": "stats", "prevSize": 32, "code": 59798, - "tempChar": "" + "tempChar": "" }, { - "order": 926, + "order": 1131, "id": 119, "name": "copy-media", "prevSize": 32, "code": 59797, - "tempChar": "" + "tempChar": "" }, { - "order": 927, + "order": 1132, "id": 120, "name": "sidebar", "prevSize": 32, "code": 59794, - "tempChar": "" + "tempChar": "" }, { - "order": 997, + "order": 1133, "id": 15, "name": "microphone", "prevSize": 32, "code": 59701, - "tempChar": "" + "tempChar": "" }, { - "order": 998, + "order": 1134, "id": 16, "name": "microphone-alt", "prevSize": 32, "code": 59707, - "tempChar": "" + "tempChar": "" }, { - "order": 1004, + "order": 1135, "id": 20, "name": "camera-add", "prevSize": 32, "code": 59663, - "tempChar": "" + "tempChar": "" }, { - "order": 1003, + "order": 1136, "id": 19, "name": "camera", "prevSize": 32, "code": 59662, - "tempChar": "" + "tempChar": "" }, { - "order": 928, + "order": 1137, "id": 121, "name": "video-stop", "prevSize": 32, "code": 59787, - "tempChar": "" + "tempChar": "" }, { - "order": 877, + "order": 1138, "id": 70, "name": "speaker-muted-story", "prevSize": 32, "code": 59850, - "tempChar": "" + "tempChar": "" }, { - "order": 876, + "order": 1139, "id": 69, "name": "speaker-story", "prevSize": 32, "code": 59844, - "tempChar": "" + "tempChar": "" }, { - "order": 929, + "order": 1140, "id": 122, "name": "speaker", "prevSize": 32, "code": 59777, - "tempChar": "" + "tempChar": "" }, { - "order": 930, + "order": 1141, "id": 123, "name": "speaker-outline", "prevSize": 32, "code": 59778, - "tempChar": "" + "tempChar": "" }, { - "order": 931, + "order": 1142, "id": 124, "name": "phone-discard-outline", "prevSize": 32, "code": 59779, - "tempChar": "" + "tempChar": "" }, { - "order": 932, + "order": 1143, "id": 125, "name": "allow-speak", "prevSize": 32, "code": 59780, - "tempChar": "" + "tempChar": "" }, { - "order": 933, + "order": 1144, "id": 126, "name": "stop-raising-hand", "prevSize": 32, "code": 59781, - "tempChar": "" + "tempChar": "" }, { - "order": 935, + "order": 1145, "id": 128, "name": "voice-chat", "prevSize": 32, "code": 59783, - "tempChar": "" + "tempChar": "" }, { - "order": 936, + "order": 1146, "id": 129, "name": "video", "prevSize": 32, "code": 59784, - "tempChar": "" + "tempChar": "" }, { - "order": 937, + "order": 1147, "id": 130, "name": "noise-suppression", "prevSize": 32, "code": 59785, - "tempChar": "" + "tempChar": "" }, { - "order": 938, + "order": 1148, "id": 131, "name": "phone-discard", "prevSize": 32, "code": 59786, - "tempChar": "" + "tempChar": "" }, { - "order": 939, + "order": 1149, "id": 132, "name": "bot-commands-filled", "prevSize": 32, "code": 59775, - "tempChar": "" + "tempChar": "" }, { - "order": 940, + "order": 1150, "id": 133, "name": "reply-filled", "prevSize": 32, "code": 59776, - "tempChar": "" + "tempChar": "" }, { - "order": 941, + "order": 1151, "id": 134, "name": "bug", "prevSize": 32, "code": 59774, - "tempChar": "" + "tempChar": "" }, { - "order": 942, + "order": 1152, "id": 135, "name": "data", "prevSize": 32, "code": 59773, - "tempChar": "" + "tempChar": "" }, { - "order": 943, + "order": 1153, "id": 136, "name": "darkmode", "prevSize": 32, "code": 59769, - "tempChar": "" + "tempChar": "" }, { - "order": 944, + "order": 1154, "id": 137, "name": "animations", "prevSize": 32, "code": 59804, - "tempChar": "" + "tempChar": "" }, { - "order": 945, + "order": 1155, "id": 138, "name": "enter", "prevSize": 32, "code": 59771, - "tempChar": "" + "tempChar": "" }, { - "order": 946, + "order": 1156, "id": 139, "name": "fontsize", "prevSize": 32, "code": 59772, - "tempChar": "" + "tempChar": "" }, { - "order": 947, + "order": 1157, "id": 140, "name": "permissions", "prevSize": 32, "code": 59766, - "tempChar": "" + "tempChar": "" }, { - "order": 948, + "order": 1158, "id": 141, "name": "card", "prevSize": 32, "code": 59767, - "tempChar": "" + "tempChar": "" }, { - "order": 949, + "order": 1159, "id": 142, "name": "truck", "prevSize": 32, "code": 59768, - "tempChar": "" + "tempChar": "" }, { - "order": 950, + "order": 1160, "id": 143, "name": "share-filled", "prevSize": 32, "code": 59738, - "tempChar": "" + "tempChar": "" }, { - "order": 951, + "order": 1161, "id": 144, "name": "bold", "prevSize": 32, "code": 59745, - "tempChar": "" + "tempChar": "" }, { - "order": 952, + "order": 1162, "id": 145, "name": "bot-command", "prevSize": 32, "code": 59746, - "tempChar": "" + "tempChar": "" }, { - "order": 953, + "order": 1163, "id": 146, "name": "calendar-filter", "prevSize": 32, "code": 59747, - "tempChar": "" + "tempChar": "" }, { - "order": 956, + "order": 1164, "id": 149, "name": "arrow-down", "prevSize": 32, "code": 59750, - "tempChar": "" + "tempChar": "" }, { - "order": 957, + "order": 1165, "id": 150, "name": "email", "prevSize": 32, "code": 59751, - "tempChar": "" + "tempChar": "" }, { - "order": 958, + "order": 1166, "id": 151, "name": "italic", "prevSize": 32, "code": 59752, - "tempChar": "" + "tempChar": "" }, { - "order": 959, + "order": 1167, "id": 152, "name": "link", "prevSize": 32, "code": 59753, - "tempChar": "" + "tempChar": "" }, { - "order": 960, + "order": 1168, "id": 153, "name": "link-broken", "prevSize": 32, "code": 59824, - "tempChar": "" + "tempChar": "" }, { - "order": 961, + "order": 1169, "id": 154, "name": "mention", "prevSize": 32, "code": 59754, - "tempChar": "" + "tempChar": "" }, { - "order": 962, + "order": 1170, "id": 155, "name": "monospace", "prevSize": 32, "code": 59755, - "tempChar": "" + "tempChar": "" }, { - "order": 964, + "order": 1171, "id": 157, "name": "password-off", "prevSize": 32, "code": 59757, - "tempChar": "" + "tempChar": "" }, { - "order": 965, + "order": 1172, "id": 158, "name": "pin-list", "prevSize": 32, "code": 59758, - "tempChar": "" + "tempChar": "" }, { - "order": 967, + "order": 1173, "id": 160, "name": "replace", "prevSize": 32, "code": 59760, - "tempChar": "" + "tempChar": "" }, { - "order": 968, + "order": 1174, "id": 161, "name": "schedule", "prevSize": 32, "code": 59761, - "tempChar": "" + "tempChar": "" }, { - "order": 969, + "order": 1175, "id": 162, "name": "strikethrough", "prevSize": 32, "code": 59762, - "tempChar": "" + "tempChar": "" }, { - "order": 970, + "order": 1176, "id": 163, "name": "underlined", "prevSize": 32, "code": 59763, - "tempChar": "" + "tempChar": "" }, { - "order": 971, + "order": 1177, "id": 164, "name": "zoom-out", "prevSize": 32, "code": 59765, - "tempChar": "" + "tempChar": "" }, { - "order": 972, + "order": 1178, "id": 165, "name": "zoom-in", "prevSize": 32, "code": 59764, - "tempChar": "" + "tempChar": "" }, { - "order": 973, + "order": 1179, "id": 67, "name": "spoiler-disable", "prevSize": 32, "code": 59829, - "tempChar": "" + "tempChar": "" }, { - "order": 974, + "order": 1180, "id": 66, "name": "grouped", "prevSize": 32, "code": 59830, - "tempChar": "" + "tempChar": "" }, { - "order": 975, + "order": 1181, "id": 65, "name": "grouped-disable", "prevSize": 32, "code": 59831, - "tempChar": "" + "tempChar": "" }, { - "order": 976, + "order": 1182, "id": 64, "name": "spoiler", "prevSize": 32, "code": 59832, - "tempChar": "" + "tempChar": "" }, { - "order": 977, + "order": 1183, "id": 63, "name": "select", "prevSize": 32, "code": 59744, - "tempChar": "" + "tempChar": "" }, { - "order": 978, + "order": 1184, "id": 61, "name": "folder", "prevSize": 32, "code": 59667, - "tempChar": "" + "tempChar": "" }, { - "order": 979, + "order": 1185, "id": 60, "name": "bots", "prevSize": 32, "code": 59669, - "tempChar": "" + "tempChar": "" }, { - "order": 980, + "order": 1186, "id": 59, "name": "calendar", "prevSize": 32, "code": 59670, - "tempChar": "" + "tempChar": "" }, { - "order": 981, + "order": 1187, "id": 58, "name": "cloud-download", "prevSize": 32, "code": 59671, - "tempChar": "" + "tempChar": "" }, { - "order": 982, + "order": 1188, "id": 57, "name": "colorize", "prevSize": 32, "code": 59672, - "tempChar": "" + "tempChar": "" }, { - "order": 983, + "order": 1189, "id": 0, "name": "forward", "prevSize": 32, "code": 59687, - "tempChar": "" + "tempChar": "" }, { - "order": 984, + "order": 1190, "id": 43, "name": "reply", "prevSize": 32, "code": 59719, - "tempChar": "" + "tempChar": "" }, { - "order": 985, + "order": 1191, "id": 3, "name": "help", "prevSize": 32, "code": 59690, - "tempChar": "" + "tempChar": "" }, { - "order": 986, + "order": 1192, "id": 4, "name": "info", "prevSize": 32, "code": 59691, - "tempChar": "" + "tempChar": "" }, { - "order": 987, + "order": 1193, "id": 54, "name": "info-filled", "prevSize": 32, "code": 59675, - "tempChar": "" + "tempChar": "" }, { - "order": 988, + "order": 1194, "id": 1, "name": "delete-filled", "prevSize": 32, "code": 59676, - "tempChar": "" + "tempChar": "" }, { - "order": 989, + "order": 1195, "id": 2, "name": "delete", "prevSize": 32, "code": 59677, - "tempChar": "" + "tempChar": "" }, { - "order": 990, + "order": 1196, "id": 5, "name": "edit", "prevSize": 32, "code": 59683, - "tempChar": "" + "tempChar": "" }, { - "order": 991, + "order": 1197, "id": 6, "name": "new-chat-filled", "prevSize": 32, "code": 59705, - "tempChar": "" + "tempChar": "" }, { - "order": 992, + "order": 1198, "id": 7, "name": "send", "prevSize": 32, "code": 59722, - "tempChar": "" + "tempChar": "" }, { - "order": 993, + "order": 1199, "id": 8, "name": "send-outline", "prevSize": 32, "code": 59723, - "tempChar": "" + "tempChar": "" }, { - "order": 999, + "order": 1200, "id": 9, "name": "poll", "prevSize": 32, "code": 59704, - "tempChar": "" + "tempChar": "" }, { - "order": 1000, + "order": 1201, "id": 10, "name": "revote", "prevSize": 32, "code": 59706, - "tempChar": "" + "tempChar": "" }, { - "order": 1001, + "order": 1202, "id": 17, "name": "photo", "prevSize": 32, "code": 59712, - "tempChar": "" + "tempChar": "" }, { - "order": 1002, + "order": 1203, "id": 18, "name": "document", "prevSize": 32, "code": 59679, - "tempChar": "" + "tempChar": "" }, { - "order": 1005, + "order": 1204, "id": 21, "name": "logout", "prevSize": 32, "code": 59698, - "tempChar": "" + "tempChar": "" }, { - "order": 1006, + "order": 1205, "id": 22, "name": "saved-messages", "prevSize": 32, "code": 59720, - "tempChar": "" + "tempChar": "" }, { - "order": 1007, + "order": 1206, "id": 24, "name": "settings", "prevSize": 32, "code": 59726, - "tempChar": "" + "tempChar": "" }, { - "order": 1008, + "order": 1207, "id": 25, "name": "phone", "prevSize": 32, "code": 59711, - "tempChar": "" + "tempChar": "" }, { - "order": 1009, + "order": 1208, "id": 26, "name": "attach", "prevSize": 32, "code": 59657, - "tempChar": "" + "tempChar": "" }, { - "order": 1010, + "order": 1209, "id": 27, "name": "copy", "prevSize": 32, "code": 59674, - "tempChar": "" + "tempChar": "" }, { - "order": 1011, + "order": 1210, "id": 68, "name": "channel-filled", "prevSize": 32, "code": 59851, - "tempChar": "" + "tempChar": "" }, { - "order": 1012, + "order": 1211, "id": 28, "name": "channel", "prevSize": 32, "code": 59665, - "tempChar": "" + "tempChar": "" }, { - "order": 1082, + "order": 1212, "id": 167, "name": "group-filled", "prevSize": 32, "code": 59852, - "tempChar": "" + "tempChar": "" }, { - "order": 1081, + "order": 1213, "id": 29, "name": "group", "prevSize": 32, "code": 59689, - "tempChar": "" + "tempChar": "" }, { - "order": 994, + "order": 1214, "id": 32, "name": "add-user-filled", "prevSize": 32, "code": 59652, - "tempChar": "" + "tempChar": "" }, { - "order": 995, + "order": 1215, "id": 33, "name": "add-user", "prevSize": 32, "code": 59653, - "tempChar": "" + "tempChar": "" }, { - "order": 996, + "order": 1216, "id": 36, "name": "delete-user", "prevSize": 32, "code": 59678, - "tempChar": "" + "tempChar": "" }, { - "order": 1015, + "order": 1217, "id": 62, "name": "non-contacts", "prevSize": 32, "code": 59688, - "tempChar": "" + "tempChar": "" }, { - "order": 1014, + "order": 1218, "id": 30, "name": "user", "prevSize": 32, "code": 59737, - "tempChar": "" + "tempChar": "" }, { - "order": 1080, + "order": 1219, "id": 166, "name": "user-filled", "prevSize": 32, "code": 59853, - "tempChar": "" + "tempChar": "" }, { - "order": 1016, + "order": 1220, "id": 31, "name": "active-sessions", "prevSize": 32, "code": 59650, - "tempChar": "" + "tempChar": "" }, { - "order": 1017, + "order": 1221, "id": 34, "name": "admin", "prevSize": 32, "code": 59654, - "tempChar": "" + "tempChar": "" }, { - "order": 966, + "order": 1222, "id": 159, "name": "previous", "prevSize": 32, "code": 59759, - "tempChar": "" + "tempChar": "" }, { - "order": 963, + "order": 1223, "id": 156, "name": "next", "prevSize": 32, "code": 59756, - "tempChar": "" + "tempChar": "" }, { - "order": 892, + "order": 1224, "id": 85, "name": "expand", "prevSize": 32, "code": 59838, - "tempChar": "" + "tempChar": "" }, { - "order": 891, + "order": 1225, "id": 84, "name": "collapse", "prevSize": 32, "code": 59837, - "tempChar": "" + "tempChar": "" }, { - "order": 1018, + "order": 1226, "id": 37, "name": "download", "prevSize": 32, "code": 59681, - "tempChar": "" + "tempChar": "" }, { - "order": 887, + "order": 1227, "id": 80, "name": "pinned-message", "prevSize": 32, "code": 59839, - "tempChar": "" + "tempChar": "" }, { - "order": 911, + "order": 1228, "id": 104, "name": "pin-badge", "prevSize": 32, "code": 59814, - "tempChar": "" + "tempChar": "" }, { - "order": 1019, + "order": 1229, "id": 38, "name": "location", "prevSize": 32, "code": 59696, - "tempChar": "" + "tempChar": "" }, { - "order": 1020, + "order": 1230, "id": 44, "name": "stop", "prevSize": 32, "code": 59730, - "tempChar": "" + "tempChar": "" }, { - "order": 890, + "order": 1231, "id": 83, "name": "archive-to-main", "prevSize": 32, "code": 59836, - "tempChar": "" + "tempChar": "" }, { - "order": 889, + "order": 1232, "id": 82, "name": "archive-from-main", "prevSize": 32, "code": 59835, - "tempChar": "" + "tempChar": "" }, { - "order": 888, + "order": 1233, "id": 81, "name": "archive-filled", "prevSize": 32, "code": 59834, - "tempChar": "" + "tempChar": "" }, { - "order": 1021, + "order": 1234, "id": 23, "name": "archive", "prevSize": 32, "code": 59656, - "tempChar": "" + "tempChar": "" }, { - "order": 1022, + "order": 1235, "id": 45, "name": "unarchive", "prevSize": 32, "code": 59731, - "tempChar": "" + "tempChar": "" }, { - "order": 906, + "order": 1236, "id": 99, "name": "chats-badge", "prevSize": 32, "code": 59809, - "tempChar": "" + "tempChar": "" }, { - "order": 905, + "order": 1237, "id": 98, "name": "chat-badge", "prevSize": 32, "code": 59808, - "tempChar": "" + "tempChar": "" }, { - "order": 923, + "order": 1238, "id": 116, "name": "share-screen", "prevSize": 32, "code": 59770, - "tempChar": "" + "tempChar": "" }, { - "order": 934, + "order": 1239, "id": 127, "name": "share-screen-outlined", "prevSize": 32, "code": 59782, - "tempChar": "" + "tempChar": "" }, { - "order": 893, + "order": 1240, "id": 86, "name": "replies", "prevSize": 32, "code": 59833, - "tempChar": "" + "tempChar": "" }, { - "order": 1023, + "order": 1241, "id": 52, "name": "readchats", "prevSize": 32, "code": 59699, - "tempChar": "" + "tempChar": "" }, { - "order": 1024, + "order": 1242, "id": 49, "name": "unread", "prevSize": 32, "code": 59735, - "tempChar": "" + "tempChar": "" }, { - "order": 1025, + "order": 1243, "id": 40, "name": "message", "prevSize": 32, "code": 59700, - "tempChar": "" + "tempChar": "" }, { - "order": 954, + "order": 1244, "id": 147, "name": "comments", "prevSize": 32, "code": 59748, - "tempChar": "" + "tempChar": "" }, { - "order": 955, + "order": 1245, "id": 148, "name": "comments-sticker", "prevSize": 32, "code": 59749, - "tempChar": "" + "tempChar": "" }, { - "order": 915, + "order": 1246, "id": 108, "name": "key", "prevSize": 32, "code": 59802, - "tempChar": "" + "tempChar": "" }, { - "order": 913, + "order": 1247, "id": 106, "name": "unlock-badge", "prevSize": 32, "code": 59816, - "tempChar": "" + "tempChar": "" }, { - "order": 914, + "order": 1248, "id": 107, "name": "lock-badge", "prevSize": 32, "code": 59817, - "tempChar": "" + "tempChar": "" }, { - "order": 1026, + "order": 1249, "id": 39, "name": "lock", "prevSize": 32, "code": 59697, - "tempChar": "" + "tempChar": "" }, { - "order": 1027, + "order": 1250, "id": 46, "name": "unlock", "prevSize": 32, "code": 59732, - "tempChar": "" + "tempChar": "" }, { - "order": 1028, + "order": 1251, "id": 41, "name": "mute", "prevSize": 32, "code": 59703, - "tempChar": "" + "tempChar": "" }, { - "order": 1029, + "order": 1252, "id": 47, "name": "unmute", "prevSize": 32, "code": 59733, - "tempChar": "" + "tempChar": "" }, { - "order": 1030, + "order": 1253, "id": 42, "name": "pin", "prevSize": 32, "code": 59713, - "tempChar": "" + "tempChar": "" }, { - "order": 1031, + "order": 1254, "id": 48, "name": "unpin", "prevSize": 32, "code": 59734, - "tempChar": "" + "tempChar": "" }, { - "order": 1032, + "order": 1255, "id": 5, "name": "smallscreen", "prevSize": 32, "code": 59742, - "tempChar": "" + "tempChar": "" }, { - "order": 1033, + "order": 1256, "id": 4, "name": "fullscreen", "prevSize": 32, "code": 59743, - "tempChar": "" + "tempChar": "" }, { - "order": 1034, + "order": 1257, "id": 0, "name": "large-pause", "prevSize": 32, "code": 59694, - "tempChar": "" + "tempChar": "" }, { - "order": 1035, + "order": 1258, "id": 1, "name": "large-play", "prevSize": 32, "code": 59695, - "tempChar": "" + "tempChar": "" }, { - "order": 1036, + "order": 1259, "id": 2, "name": "pause", "prevSize": 32, "code": 59709, - "tempChar": "" + "tempChar": "" }, { - "order": 1037, + "order": 1260, "id": 3, "name": "play", "prevSize": 32, "code": 59715, - "tempChar": "" + "tempChar": "" }, { - "order": 1038, + "order": 1261, "id": 0, "name": "channelviews", "prevSize": 32, "code": 59666, - "tempChar": "" + "tempChar": "" }, { - "order": 1039, + "order": 1262, "id": 1, "name": "message-succeeded", "prevSize": 32, "code": 59648, - "tempChar": "" + "tempChar": "" }, { - "order": 1040, + "order": 1263, "id": 2, "name": "message-read", "prevSize": 32, "code": 59649, - "tempChar": "" + "tempChar": "" }, { - "order": 1041, + "order": 1264, "id": 3, "name": "message-pending", "prevSize": 32, "code": 59724, - "tempChar": "" + "tempChar": "" }, { - "order": 1042, + "order": 1265, "id": 4, "name": "message-failed", "prevSize": 32, "code": 59725, - "tempChar": "" + "tempChar": "" }, { - "order": 1043, + "order": 1266, "id": 13, "name": "favorite", "prevSize": 32, "code": 59710, - "tempChar": "" + "tempChar": "" }, { - "order": 1044, + "order": 1267, "id": 10, "name": "keyboard", "prevSize": 32, "code": 59716, - "tempChar": "" + "tempChar": "" }, { - "order": 1045, + "order": 1268, "id": 12, "name": "delete-left", "prevSize": 32, "code": 59717, - "tempChar": "" + "tempChar": "" }, { - "order": 1046, + "order": 1269, "id": 6, "name": "recent", "prevSize": 32, "code": 59718, - "tempChar": "" + "tempChar": "" }, { - "order": 1047, + "order": 1270, "id": 9, "name": "gifs", "prevSize": 32, "code": 59727, - "tempChar": "" + "tempChar": "" }, { - "order": 1048, + "order": 1271, "id": 11, "name": "stickers", "prevSize": 32, "code": 59739, - "tempChar": "" + "tempChar": "" }, { - "order": 1049, + "order": 1272, "id": 7, "name": "smile", "prevSize": 32, "code": 59728, - "tempChar": "" + "tempChar": "" }, { - "order": 1050, + "order": 1273, "id": 0, "name": "animals", "prevSize": 32, "code": 59655, - "tempChar": "" + "tempChar": "" }, { - "order": 1051, + "order": 1274, "id": 2, "name": "eats", "prevSize": 32, "code": 59682, - "tempChar": "" + "tempChar": "" }, { - "order": 1052, + "order": 1275, "id": 8, "name": "sport", "prevSize": 32, "code": 59729, - "tempChar": "" + "tempChar": "" }, { - "order": 1053, + "order": 1276, "id": 1, "name": "car", "prevSize": 32, "code": 59664, - "tempChar": "" + "tempChar": "" }, { - "order": 1054, + "order": 1277, "id": 4, "name": "lamp", "prevSize": 32, "code": 59692, - "tempChar": "" + "tempChar": "" }, { - "order": 1055, + "order": 1278, "id": 5, "name": "language", "prevSize": 32, "code": 59693, - "tempChar": "" + "tempChar": "" }, { - "order": 1056, + "order": 1279, "id": 3, "name": "flag", "prevSize": 32, "code": 59686, - "tempChar": "" + "tempChar": "" }, { - "order": 1057, + "order": 1280, "id": 0, "name": "more", "prevSize": 32, "code": 59702, - "tempChar": "" + "tempChar": "" }, { - "order": 1058, + "order": 1281, "id": 1, "name": "search", "prevSize": 32, "code": 59721, - "tempChar": "" + "tempChar": "" }, { - "order": 1059, + "order": 1282, "id": 9, "name": "remove", "prevSize": 32, "code": 59740, - "tempChar": "" + "tempChar": "" }, { - "order": 1060, + "order": 1283, "id": 2, "name": "add", "prevSize": 32, "code": 59651, - "tempChar": "" + "tempChar": "" }, { - "order": 1061, + "order": 1284, "id": 3, "name": "check", "prevSize": 32, "code": 59668, - "tempChar": "" + "tempChar": "" }, { - "order": 1062, + "order": 1285, "id": 4, "name": "close", "prevSize": 32, "code": 59673, - "tempChar": "" + "tempChar": "" }, { - "order": 1063, + "order": 1286, "id": 5, "name": "arrow-left", "prevSize": 32, "code": 59661, - "tempChar": "" + "tempChar": "" }, { - "order": 1064, + "order": 1287, "id": 6, "name": "arrow-right", "prevSize": 32, "code": 59708, - "tempChar": "" + "tempChar": "" }, { - "order": 1065, + "order": 1288, "id": 7, "name": "down", "prevSize": 32, "code": 59680, - "tempChar": "" + "tempChar": "" }, { - "order": 1066, + "order": 1289, "id": 8, "name": "up", "prevSize": 32, "code": 59736, - "tempChar": "" + "tempChar": "" }, { - "order": 1067, + "order": 1290, "id": 20, "name": "eye-closed", "prevSize": 32, "code": 59685, - "tempChar": "" + "tempChar": "" }, { - "order": 1068, + "order": 1291, "id": 19, "name": "eye", "prevSize": 32, "code": 59684, - "tempChar": "" + "tempChar": "" }, { - "order": 1069, + "order": 1292, "id": 4, "name": "muted", "prevSize": 32, "code": 59741, - "tempChar": "" + "tempChar": "" }, { - "order": 1070, + "order": 1293, "id": 0, "name": "avatar-archived-chats", "prevSize": 32, "code": 59658, - "tempChar": "" + "tempChar": "" }, { - "order": 1071, + "order": 1294, "id": 1, "name": "avatar-deleted-account", "prevSize": 32, "code": 59659, - "tempChar": "" + "tempChar": "" }, { - "order": 1072, + "order": 1295, "id": 2, "name": "avatar-saved-messages", "prevSize": 32, "code": 59660, - "tempChar": "" + "tempChar": "" }, { - "order": 1073, + "order": 1296, "id": 3, "name": "pinned-chat", "prevSize": 32, "code": 59714, - "tempChar": "" + "tempChar": "" } ], "prevSize": 32, @@ -5154,4 +5348,4 @@ "showLiga": false }, "uid": -1 -} +} \ No newline at end of file diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 27140456a..6a66c8e02 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -13,6 +13,17 @@ margin-inline-end: calc($margin - var(--scrollbar-width)); } +@mixin gradient-border-top($width, $cutout: 0px) { + mask-image: linear-gradient(transparent $cutout, black $width); +} + +@mixin gradient-border-bottom($width, $cutout: 0px) { + mask-image: linear-gradient(to top, transparent $cutout, black $width); +} + +@mixin gradient-border-top-bottom($top, $bottom) { + mask-image: linear-gradient(transparent 0%, black $top, black calc(100% - $bottom), transparent 100%); +} @mixin reset-range() { input[type="range"] { diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 0aa51e8a5..1fa995831 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -198,6 +198,8 @@ $color-message-story-mention-to: #74bcff; --color-deleted-account: #9eaab5; --color-archive: #9eaab5; + --color-heart: #ff3c32; + --vh: 1vh; --border-radius-default: 0.75rem; @@ -234,6 +236,7 @@ $color-message-story-mention-to: #74bcff; } --z-modal-confirm: 10000; + --z-portal-menu: 10000; --z-symbol-menu-modal: 5000; --z-lock-screen: 3000; --z-ui-loader-mask: 2000; diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 2b1a8e471..4ba7a87a2 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -31,6 +31,33 @@ display: block; } +.icon-eye-outline:before { + content: "\e9cf"; +} +.icon-eye-closed-outline:before { + content: "\e9d3"; +} +.icon-story-caption:before { + content: "\e9d4"; +} +.icon-story-priority:before { + content: "\e9ce"; +} +.icon-stealth-past:before { + content: "\e9d0"; +} +.icon-stealth-future:before { + content: "\e9d1"; +} +.icon-timer:before { + content: "\e9d2"; +} +.icon-arrow-down-circle:before { + content: "\e9d5"; +} +.icon-story-reply:before { + content: "\e9d6"; +} .icon-loop:before { content: "\e98c"; } diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index dc2149df4..59f3dbd34 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -27,6 +27,7 @@ export const processDeepLink = (url: string) => { processAttachBotParameters, openChatWithDraft, checkChatlistInvite, + openStoryViewerByUsername, } = getActions(); // Safari thinks the path in tg://path links is hostname for some reason @@ -37,7 +38,7 @@ export const processDeepLink = (url: string) => { case 'resolve': { const { domain, phone, post, comment, voicechat, livestream, start, startattach, attach, thread, topic, - appname, startapp, + appname, startapp, story, } = params; const startAttach = params.hasOwnProperty('startattach') && !startattach ? true : startattach; @@ -64,6 +65,8 @@ export const processDeepLink = (url: string) => { }); } else if (phone) { openChatByPhoneNumber({ phoneNumber: phone, startAttach, attach }); + } else if (story) { + openStoryViewerByUsername({ username: domain, storyId: Number(story) }); } else { openChatByUsername({ username: domain, diff --git a/src/util/fallbackLangPack.ts b/src/util/fallbackLangPack.ts index e8c630dec..8e83228da 100644 --- a/src/util/fallbackLangPack.ts +++ b/src/util/fallbackLangPack.ts @@ -502,4 +502,6 @@ export default { FoldersAllChatsDesc: 'All unarchived chats', 'Video.Unsupported.Desktop': 'Unfortunately, this video can\'t be played on Telegram Web. Try opening it with our **desktop app** instead.', 'Video.Unsupported.Mobile': 'Unfortunately, this video can\'t be played on Telegram Web. Try opening it with our **mobile app** instead.', + SlowModeWait: 'Slow Mode — %d', + OpenMapWith: 'Open map with...', } as ApiLangPack; diff --git a/src/util/hoc/freezeWhenClosed.ts b/src/util/hoc/freezeWhenClosed.ts new file mode 100644 index 000000000..67744d4f4 --- /dev/null +++ b/src/util/hoc/freezeWhenClosed.ts @@ -0,0 +1,20 @@ +import { useRef, type FC, type Props } from '../../lib/teact/teact'; + +export default function freezeWhenClosed(Component: T) { + function ComponentWrapper(props: Props) { + const newProps = useRef(props); + + if (props.isOpen) { + newProps.current = props; + } else { + newProps.current = { + ...newProps.current, + isOpen: false, + }; + } + + return Component(newProps.current); + } + + return ComponentWrapper as T; +} diff --git a/src/util/map.ts b/src/util/map.ts index 3b64f3545..3be8e49d9 100644 --- a/src/util/map.ts +++ b/src/util/map.ts @@ -1,4 +1,11 @@ -const PROVIDER = 'https://maps.google.com/maps'; +import type { ApiGeoPoint } from '../api/types'; + +const PROVIDERS = { + google: 'https://maps.google.com/maps', + bing: 'https://bing.com/maps/default.aspx', + osm: 'https://www.openstreetmap.org', + apple: 'https://maps.apple.com', +}; // https://github.com/TelegramMessenger/Telegram-iOS/blob/2a32c871882c4e1b1ccdecd34fccd301723b30d9/submodules/LocationResources/Sources/VenueIconResources.swift#L82 const VENUE_COLORS = new Map(Object.entries({ @@ -27,8 +34,20 @@ const RANDOM_COLORS = [ '#e56cd5', '#f89440', '#9986ff', '#44b3f5', '#6dc139', '#ff5d5a', '#f87aad', '#6e82b3', '#f5ba21', ]; -export function prepareMapUrl(lat: number, long: number, zoom: number) { - return `${PROVIDER}/place/${lat}+${long}/@${lat},${long},${zoom}z`; +export function prepareMapUrl(provider: keyof typeof PROVIDERS, point: Omit, zoom = 15) { + const { lat, long } = point; + const providerUrl = PROVIDERS[provider]; + switch (provider) { + case 'google': + return `${providerUrl}/place/${lat}+${long}/@${lat},${long},${zoom}z`; + case 'bing': + return `${providerUrl}?cp=${lat}~${long}&lvl=${zoom}&sp=point.${lat}_${long}`; + case 'apple': + return `${providerUrl}?q=${lat},${long}`; + case 'osm': + default: + return `${providerUrl}/?mlat=${lat}&mlon=${long}&zoom=${zoom}`; + } } export function getMetersPerPixel(lat: number, zoom: number) {