From a7c7c8d95c0913fd6903e5897813e3325aedfa7d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 4 Sep 2023 04:05:50 +0200 Subject: [PATCH] Stories: New features and fixes (#3773) --- src/api/gramjs/apiBuilders/bots.ts | 2 +- src/api/gramjs/apiBuilders/chats.ts | 2 +- src/api/gramjs/apiBuilders/common.ts | 117 +- src/api/gramjs/apiBuilders/messageContent.ts | 644 +++++++++ src/api/gramjs/apiBuilders/messages.ts | 918 +------------ src/api/gramjs/apiBuilders/misc.ts | 3 +- src/api/gramjs/apiBuilders/payments.ts | 3 +- src/api/gramjs/apiBuilders/reactions.ts | 103 ++ src/api/gramjs/apiBuilders/stories.ts | 144 ++ src/api/gramjs/gramjsBuilders/index.ts | 11 +- src/api/gramjs/methods/index.ts | 8 +- src/api/gramjs/methods/media.ts | 8 +- src/api/gramjs/methods/messages.ts | 8 +- src/api/gramjs/methods/reactions.ts | 2 +- src/api/gramjs/methods/settings.ts | 32 +- src/api/gramjs/methods/stories.ts | 89 +- src/api/gramjs/methods/symbols.ts | 2 +- src/api/gramjs/updater.ts | 69 +- src/api/types/messages.ts | 2 +- src/api/types/stories.ts | 41 +- src/api/types/updates.ts | 23 +- src/assets/fonts/icomoon.woff | Bin 62616 -> 66940 bytes src/assets/fonts/icomoon.woff2 | Bin 28636 -> 30872 bytes src/bundles/extra.ts | 1 + .../calls/group/GroupCallParticipantVideo.tsx | 2 +- .../calls/group/MicrophoneButton.tsx | 3 +- .../calls/group/OutlinedMicrophoneIcon.tsx | 3 +- src/components/common/AnimatedSticker.tsx | 23 +- src/components/common/Avatar.scss | 11 + src/components/common/Avatar.tsx | 6 +- src/components/common/AvatarStoryCircle.tsx | 12 +- src/components/common/Composer.scss | 13 + src/components/common/Composer.tsx | 123 +- src/components/common/CustomEmoji.tsx | 3 + src/components/common/CustomEmojiPicker.tsx | 3 + src/components/common/DeleteChatModal.tsx | 6 +- src/components/common/EmbeddedMessage.scss | 3 +- src/components/common/EmbeddedStory.tsx | 5 +- .../common/FullNameTitle.module.scss | 2 +- src/components/common/MessageText.tsx | 3 + src/components/common/ReactionEmoji.tsx | 3 + .../common/ReactionStaticEmoji.scss | 10 + src/components/common/ReactionStaticEmoji.tsx | 18 +- src/components/common/StickerButton.tsx | 3 + src/components/common/StickerSet.tsx | 24 +- src/components/common/StickerView.tsx | 6 +- .../common/helpers/renderMessageText.ts | 33 +- .../common/helpers/renderTextWithEntities.tsx | 60 +- src/components/left/main/Chat.scss | 4 +- .../left/main/hooks/useChatListEntry.tsx | 3 +- .../left/settings/BlockUserModal.tsx | 10 +- .../SettingsActiveWebsites.module.scss | 2 +- .../left/settings/SettingsActiveWebsites.tsx | 2 +- .../left/settings/SettingsPrivacy.tsx | 6 +- .../settings/SettingsPrivacyBlockedUsers.tsx | 16 +- src/components/main/Main.tsx | 12 + .../premium/PremiumFeatureItem.module.scss | 9 +- .../main/premium/PremiumFeatureItem.tsx | 19 +- .../premium/PremiumFeatureModal.module.scss | 10 +- .../main/premium/PremiumFeatureModal.tsx | 12 + .../main/premium/PremiumMainModal.module.scss | 2 +- .../main/premium/PremiumMainModal.tsx | 1 + .../PremiumFeaturePreviewStories.module.scss | 43 + .../previews/PremiumFeaturePreviewStories.tsx | 123 ++ src/components/mediaViewer/MediaViewer.tsx | 2 +- .../mediaViewer/MediaViewerContent.tsx | 2 +- src/components/middle/ChatReportPanel.tsx | 5 +- .../middle/EmojiInteractionAnimation.tsx | 2 +- src/components/middle/MessageListBotInfo.tsx | 2 +- .../middle/composer/AttachmentModal.tsx | 1 + .../middle/composer/MessageInput.tsx | 19 +- .../middle/composer/StickerPicker.tsx | 3 + .../middle/composer/StickerSetCover.tsx | 4 + src/components/middle/composer/SymbolMenu.tsx | 14 +- .../middle/composer/SymbolMenuButton.tsx | 4 +- .../composer/hooks/useClipboardPaste.ts | 6 +- .../middle/message/AnimatedEmoji.tsx | 2 +- src/components/middle/message/Game.tsx | 2 +- src/components/middle/message/Invoice.tsx | 2 +- src/components/middle/message/Location.tsx | 14 +- .../middle/message/MessageContextMenu.tsx | 2 +- .../middle/message/ReactionAnimatedEmoji.tsx | 4 +- .../middle/message/ReactionPicker.tsx | 62 +- .../message/ReactionSelectorReaction.tsx | 2 + .../middle/message/helpers/copyOptions.ts | 2 +- .../modals/mapModal/MapModal.async.tsx | 17 + .../modals/mapModal/MapModal.module.scss | 10 + src/components/modals/mapModal/MapModal.tsx | 100 ++ src/components/payment/Checkout.tsx | 2 +- src/components/right/Profile.tsx | 4 +- src/components/story/MediaAreaOverlay.tsx | 55 + .../story/StealthModeModal.module.scss | 53 + src/components/story/StealthModeModal.tsx | 135 ++ src/components/story/Story.tsx | 151 ++- src/components/story/StoryCaption.tsx | 85 +- .../story/StorySettings.module.scss | 7 +- src/components/story/StorySettings.tsx | 38 +- src/components/story/StorySlides.tsx | 26 +- src/components/story/StoryView.tsx | 152 +++ .../story/StoryViewModal.module.scss | 125 ++ src/components/story/StoryViewModal.tsx | 293 +++++ src/components/story/StoryViewer.module.scss | 156 ++- src/components/story/StoryViewer.tsx | 30 +- src/components/story/StoryViewers.tsx | 159 --- src/components/story/helpers/dimensions.ts | 18 +- src/components/story/hooks/useSlideSizes.ts | 4 +- src/components/story/hooks/useStoryProps.ts | 14 +- src/components/ui/ListItem.tsx | 10 +- src/components/ui/Menu.scss | 4 + src/components/ui/Menu.tsx | 15 +- src/components/ui/Modal.tsx | 12 +- src/components/ui/TextTimer.tsx | 44 + .../PlaceholderChatInfo.module.scss | 50 + .../ui/placeholder/PlaceholderChatInfo.tsx | 19 + .../ui/{ => placeholder}/Skeleton.scss | 0 .../ui/{ => placeholder}/Skeleton.tsx | 8 +- src/config.ts | 10 +- src/global/actions/api/bots.ts | 8 +- src/global/actions/api/chats.ts | 7 +- src/global/actions/api/payments.ts | 2 + src/global/actions/api/settings.ts | 56 +- src/global/actions/api/stories.ts | 101 +- src/global/actions/apiUpdaters/misc.ts | 28 +- src/global/actions/ui/misc.ts | 19 + src/global/actions/ui/reactions.ts | 2 + src/global/actions/ui/stories.ts | 94 +- src/global/cache.ts | 4 + src/global/helpers/media.ts | 45 +- src/global/helpers/messageMedia.ts | 2 +- .../helpers/renderMessageSummaryHtml.ts | 2 +- src/global/initialState.ts | 1 + src/global/reducers/settings.ts | 4 +- src/global/reducers/stories.ts | 108 +- src/global/selectors/stories.ts | 6 - src/global/types.ts | 77 +- src/hooks/useHistoryBack.ts | 5 - src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 177 ++- src/lib/gramjs/tl/apiTl.js | 37 +- src/lib/gramjs/tl/static/api.json | 4 +- src/lib/gramjs/tl/static/api.tl | 41 +- src/lib/rlottie/RLottie.ts | 16 +- src/styles/Telegram T.json | 1160 ++++++++++------- src/styles/_mixins.scss | 11 + src/styles/_variables.scss | 3 + src/styles/icons.scss | 27 + src/util/deeplink.ts | 5 +- src/util/fallbackLangPack.ts | 2 + src/util/hoc/freezeWhenClosed.ts | 20 + src/util/map.ts | 25 +- 150 files changed, 4775 insertions(+), 2172 deletions(-) create mode 100644 src/api/gramjs/apiBuilders/messageContent.ts create mode 100644 src/api/gramjs/apiBuilders/reactions.ts create mode 100644 src/api/gramjs/apiBuilders/stories.ts create mode 100644 src/components/main/premium/previews/PremiumFeaturePreviewStories.module.scss create mode 100644 src/components/main/premium/previews/PremiumFeaturePreviewStories.tsx create mode 100644 src/components/modals/mapModal/MapModal.async.tsx create mode 100644 src/components/modals/mapModal/MapModal.module.scss create mode 100644 src/components/modals/mapModal/MapModal.tsx create mode 100644 src/components/story/MediaAreaOverlay.tsx create mode 100644 src/components/story/StealthModeModal.module.scss create mode 100644 src/components/story/StealthModeModal.tsx create mode 100644 src/components/story/StoryView.tsx create mode 100644 src/components/story/StoryViewModal.module.scss create mode 100644 src/components/story/StoryViewModal.tsx delete mode 100644 src/components/story/StoryViewers.tsx create mode 100644 src/components/ui/TextTimer.tsx create mode 100644 src/components/ui/placeholder/PlaceholderChatInfo.module.scss create mode 100644 src/components/ui/placeholder/PlaceholderChatInfo.tsx rename src/components/ui/{ => placeholder}/Skeleton.scss (100%) rename src/components/ui/{ => placeholder}/Skeleton.tsx (81%) create mode 100644 src/util/hoc/freezeWhenClosed.ts 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 41442d299056238cf8e0fa32137c947ba255984f..51b714884db377b652e77c4bdb1038a156a2af04 100644 GIT binary patch delta 4539 zcmbtYS&SS<8Lp$ds=B-Sn4`OAcg~&Jneoo-F?-C+Zfx)DTD#u0Rh zI85B2p75sUZacPiTG5_@-(vIej-5Dn6lhN)^ajkmanF^^uPz^d-`ZUWZOi~qM?JO2 z&!0XHM4+93-^1!L=2-Ih$unnx_Ao*e==8)LFWvO)iBos3A$$nG0ZM}md>_z$3p}4x&zJTauita(^cgtMt#9&C7*Tp)@9ZmR>dO_}NK zGwrFF_KecjSL$?FDmtY<6%;WXBHBZ=_e~*K0m$uEtzKh%OB0;(DpB{R+ZDWP=kex_ zDT7i`&W_)=j}MxT!~=uVyL2|x z;~3k9&w0U?SS(=gU^4*r#y1XWZkJ3fZB`x+q>d*N`xJY&>)982N`bF;csIF1eT- zJ3KVEpet$ktYU{6x-J?+{Ys;8%jjsW6ovzYn6MzF1g~IR9!`Ln4cNA8WjF&oEI4?$ zG7RSzBB2>?oz-nZPU})LTv?Qwj^S`EPljCgwSzkBp6RZ9jtdUohgS}0p2u&aIwq_y zD{epl$b|F@Nm!0VcqtHxw&ihU)VLY$aTv3 zNLX!*ynAAzb!6A6iDU_5ZnrNSv=7(^FSKp&&{DE}?_PV4d#g2M`BgKZ+Gwrb+OcNVV*vk5&BZKTE#jcF8^_T5F;bNn zs`iWm3xMWqec_hDe!Wf0$|yjeo>BsUG$av~CLG90wQg&s4FWsyRN$vs6LUy+zzE|C zIg+vNHqE=O%ye_KmrQ0d*Uxqrdb8adGMW9mB{M``R*QvEY2^z?mzNG5T3$YyFNDA~ zw>VdtG6|7ZUx52%l7225$P^dSaBM7a(mP|?w)uXBMbWRd=EmrfBl$rd!XurA%;pYH zPIeZ$v)5rp8dxRBqiJ>T<$kQQYm zs5Hx&dZCMzVc0tj_g1^r-)6XTs0jh_9xxo#^bksWsEk*@>z6~{=*GV8^YzQEY@bgB zd6-cS9hp$glFH8Gjk1}yOtU;Pz8%Tz;H~q$)wJuTE4|q*_frW5n z-89l6=}yr{=nyntN719`+vsNqrJ_Q=nQPyCLCTx~FP+#-s&Q`s1PE?uQ&eA3!2Kw_ zs$d5a`15wAmzG%TW z+n#<@5mf$B1OAvMsK6ZOaCD6mHG4C!LKSg|n_|3N{NwUsZ^F zFU4ogR54%54lJITP^Dm3$V^OxXS&$X@oavSQKHd{OqQPDRJT?pgLaCtz_&D!6>@;- z%t{4<&>_M#Gil;{?!CQ*l_QILZcO`Fvur~eS~cIZ2bZdejGN3R=H5M!^1MW!FV82% zlcLv&i$cDWawcVJN9cv_$W9yj8x1sy1UF%z3#jR&o|+GIM~Apb_*;dB+Jejyonno7 zH!eU$F_%)hfw2h(;QBmo7G&0z7lC6UJrhi>SWphJ2z;Y2bT%lB0j|kZz*R%d(1TFb z&(boQK+EU|I*lGeUxP@BP49#4(~;%lJ~1*ISWacS$;e5dK!7oM7+ZQ0jK5V>>a{XZ zLA;2EVpp4pTGdMIQ&)Xs%X^?3dK>#txzYentHj&brbm%68XQXrTZ4j0?Y*N?=z_aR zCuvBiuq8mGe7nDC`D#^^qha8-+|X|A+fUBeAx8oH%%uxNtny2&$! znSy(R_S3g>{$(3MIv{$rrd3c5&8kZuE-TpAfAOaPSJt2XX08tm;-RWGbIpvrwW>b`t}AVyd2Z&9v|bqSejFo(C! zFHuJ2fOH4pGUPdVS2MKh9+@>fk@bUu5W*-0VO|qjp;9^AXpXsg$4e73=z`4ieY0qX zjf4p|w36XgQnqIP(1UA<`V4qpO5GC(NmYTKXoPmr1kO~&HA-^hp~dmBolue?2*BU8 zOjumoOEk@5Jmr1PFhC3&#^<~gXO_BS6i8CsIK;*kl2V6v^RC+U zfG0Dy%t&uwIK?Y9Yx)+6^1A>~# zFS;()HI@nDf0ZEg#BJYw^mcdGYsk`+Z=uWPqu$ng{X4I8@$;8nd}Wmm!>r;sr8UuG z@P1(PIR)3 delta 329 zcmey<#WLe1vsk&mn;Qco0}y1di+spde5T zq*n!mIW5^ZGjdBRfMO3A7??dk*lu6Y+noGlpgO;g8bCvrfpGV$0D;`Z3ZTWGE&w$u zfbnC-uX%~NsX(z0Ks9Y3JkyKwcR_w}3D848L!s)Jfh=a`f0KC_?HSKb_F?p9JiB=Y zR@iqhF`nP%D+4zRP=JBq!P6uj7(F@SpE={D%@h84FmM0E$heFV28wZgg8(h*Q^YF$>r@V9eH^Ap8H%$&InV06Ws4h?k1G=#ILfihxHOf2kF|A(HsU8Sse8_4_d}cIjI<62yaB^Q72rA@b*UeAr33N-`GK8(H8ZW$?%EGX zZ2;?op`2Qpq(_pIBT3VC36maD(nCnofxv>px0~fG`(u5j?`TF4C`guU85RtjG9^A3 z)amzq>+7%`amx>Y*Sq^L)3Qpdw91kxcERRA!H{qf0s@QH2KBx~O4CYxvqTb45T6RE zk&@ZWEP|Uzpw=;u>u_*xE$tya6nEgFf|m?~b4$0Z>rhOK%|y*$hjz~4V7K3yXMb*f71K&hrl0zVFsW`fYNe7 zN`?khg2XjJaaTi-UQ=>PNz2WYL&{B+nkM9`+j5U9H@%C>^RKG8eyI2WS16TEm99!o zbA5Z#$4izm!}R|LNd5l-${2bAzxUqcrq9G=m z;4-HDzX0*1*IH$2A#``VTdV4$dUvSmhIPD(?pzE%6=JRk;0ZO zy84fpt9l&RAJ2`G99J1dB^e^28#-230sh~E0RY=@v8{4b^vD~;^(175GPA8uP5=Sn zl47uv0Vw?!Q3EhydM&b9M(;*Xf{%az#s2WhP=ymc0P)~H>WF203sD;RV?fz3O+6iv z9S&v+@q07SeNh4d2vbjPdvM?Kzr_+l7y_-fXO0-+g2aMD?*+)!kRnY4=?njtK>!BA zZUk)m5A3|s(Sx6%xpLe`e;2a0$k75JK$C)4 zjANF~0i?)2vo0`3zTmi)83126AJ+;)BpOx@Q8FE+(rD;FM=JBC1>gZ@uUX1AqjfpN z7*@;(ok8$ts#d0EU*e!40ByYH90u~?=?Q?b7Pfg?bKATQuon8hhWex?IV|}A;e5Un zAdoQR@`{uMYJi9FFdoG#M-HP2Zs&F>Etk{o2`SP;8)$q$XFiiBa@Dz0#V%9?2$2{A zAd&$!gL0wJXX9DURTPrP?9F<+Xy^+w;;ATqwRRDP2v37=lJlIAy`-9m7-7 zqQt9%J2=vc5$%tS!*r8}rq-SY`u7p^ED@`SE&)7fjS+?E)j;|j$jG;KH0r_a9=$8BQK(OM?=II>pBdu3*25xB$%ZLsYH8cCt0+1{V)kIOCyR($nJffEdH}o;r$f7h`?j^y{nZb-a4H z-P&AUzD$oEU*U<-eIq8J1QA3jW!!mP%2RX4xZo<9)HPpwb%baGj1?B%<0OF`=^>fQ zy}K3`CG3=Xy08DM$oMlxXQGAqeeCW0LKBG87n*(i1FEZqQi-zeEMRy3=qv!%OB*vu zZrJtfrx8IL66J-XiFC!#MU98Gc6@KHz_Lbd=&4h?PxZSyl-nzt5Uwc8b*=g}@s{bB zHika}Fp8Bm5xQ+qU1nbBgu|pyiH6f2dZcl>;<1bzGgdiewtxVt_?8Il+%%{d3B~IY zDFr90aEvAfk?U{Vgk&rI!4jd_=6J+lNtyR zV3t?}_%>b!{T ztVrLOkTjwN#r3aEj!2NCFw1WpMUpUVL#V6O!I{yuv#^{}&M&#hV`jUZC$i!)k≺ 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 T@;|2j$NyJ8f@RDMOYwgJcuaeh literal 28636 zcmV(}K+wN;Pew8T0RR910B_s?4FCWD0R89y0B?f;0RR9100000000000000000000 z0000#Mn+Uk92y=5U;u(%5eN!{*EE9bJ_~~=00A}vBm;&_1Rw>28wZd_8)e0Hh7@cZ zKsGjfhKMTKsUL|5HV%N0`Gwj4|2;v;xb4?&N(E@}V1<sC+Z@s8-Fx~>FMfAY)x=ikYW(AXzQ?$6VlzW4um?vl$zAZS9M zl0X*{sA#Z~QbTf~RH1d~3bZ~W6`#;LooT}ys!XL0%#7+x=sd&obL%aV+TUQX!C=W? z)L78Mn$ZLHM~Q622$e|d*f0n|#Xv!1`NjKsRrO^;wQVlZM0 zeR8j^Ud`$@Ah17rKU4uO+B8jGY-xdfOwN5Zo_3}+1dv+b`T)NF_RiViDHhp1#v^R+ z$?P6&u~&7K@eb0~!`z?7|KBJ3Ig1KW+gBLM?8jcZkbz4*Jv<_5?A@2h6* z{~)OWBnQZm+9)|v8#O-1y)+wB(``|`ci;QzzW+hs0ib{nPyzu;=0L%~2c-c(N+u+Z z1XA)WCuey!$=;MekQz~tya(cNY!2nXk+bYgIkmBpmRu5_FK$NQYrIbOo{dYt5 zPhMgoUl9-|QA0LhL^+I$Tx4oJIp~+(n(5435QdE!dJH8U*CxNTEYw zQX~I&1_2n5OA#<02PeVT?Z?Ob&bsBZKdz<|oF zk^=AmbkBW2VrEjMBz%80sv20H0tZ?4xyGg2rO@DcD7W)zB!p-- zF~DZvwg8Y}v4FIo3s(M;5WIL0)%l%V3!s?dI-Mqj9P&iu@hFhq-oBJXs)t=EWN26T zX?41dn4^<G&BDD+6 zZTzFuSIb)GwRIds|2!~0p3&cj7Hg=s|bc-QV&RM~jE2>jj_emMJg=wLRL`b(6;|$1E!4eHck&HHO zL!y)6c!}67haL~BYY8$H1b)DJF{MIYsyUDe(GJKGGE*)L(RUq=Jc5F!6L21a>DJI1b%4 zJk841ir3&40nB&FTU)RJKqm!D5igp?HeT?#AYcLio|&Eu1&_{u4#gWBF_G;k%2Rj9 zb4Ajy6v_SVqbOH6?v5C*w7=*>w({LJt$#>wo4>Z@)JPIVmpvuSkQWr^S4$%v795;c zi?Vg`SHE6Qq-k|csz;@VTjF$Ps`8hgu8Qd$LEqn_{`yMP7L&U2x`(Gf=1pV{kA;5Q zv0T5JmGYOPb5qkgn8~te!Di1b zI8F;oLL)f(UJ`2;wyZxptF_NL3!&m#Q4UYaAq2K+itx^61PJeQuJ*(<{DBl zlq9L@_gT`IT?AJl%`;K3z=h^Iu$VUW9|r!?m7aHoKl%ie(bVysed0S!h2^*%8d(|; z`^^gLN&yimD$#E`&2^;YO*AE`bu)~4G1DC8qH>6-z$_t%Sgw-F(E{aEe=XvsH#YR& zay~U<MiBN48xMzOYcXJwYuz!3253ej4;BJ`0utX!l@i)|tSc|9hK72`w?{_L= zOEV4+3nB6CYihkEV35prH6FeXN!-v0IdI%Ag;_-$P`k7oXCjC4b^~)kIl4X}MFqsG=!WhyS$mLJ_ zEOI_tnmLn8+$&Gx#oP4p^4y)eg%7KXY-L$6k*nl?p@eT{85W92OR96xM1h)L1R)qm zh=gfZY^KP|hEzMaVgXaBWPwsD8@-{-T=r@VLbexXE81k6r!_9_Cu-qr8*kXJqy1jO z?2&-V`F2&4|1j6l?vUgoilVs);v~)(%~Y_RN9ynHTYPGZ>8xPLHSD&1#{>;P0WmsBAq%1U+iYCPXR3qXig5Oh!WtU8I$~ z^V>G|5~6!mT)+5;-*>2g?1nCCiWs_<08J<^FgS%n97a0gjVAC46L-)>EWXN%OC|+- z0URSYGDh}8w94PgkQ1Ezs@PHSB$W{{?g<>bI7g{4OpN*thf@9}-V;8Sq~xC@mn5-k z9)yNVB|P;R0q+Jyu#^UDC_4IsUc!h$GXbRlsf|5!PzCHsh^u-Iqz;(Mp}M1x>1ET{ z0|<^P4agR3_&$fo2HKkDHnR|ozKAB;!QAqA>7*0`F}8XhC%Fg@Xcgs#|BBa3s!@vCSYZ zN?k?mE?|vokB%WmC}V3EOMSGX0oJ;m=Gqt+TPSTZ}k>Gp@ACblimBLRdT=W#m9aumh&= zkED1my+LPrkB}-tGbwAjuD1oCge9&k>#@dk15MDL&M z+|u*VnmRAyF3V`MV14cQ$3qa67l(&yxp1_t!%^f3t62#R*&;YDd5rQ zFw}O`9V%+tQ@+8{XmT&n7xEW)+g9Q@^%H4q9FAjdn7UrmYJA^GXi15&)bCYBz0=R1 z-hnM<<^-;EeN0E15tub&lw}<)tU}Cpim3yuT>6y6|3AwZk?^Oy2VS# zFhsbQ-2(9H5*wYn+j*`me0a-ZN8jFoc=1CO zTv@N{&BL6&^*;2vP_kZO!j}!!OAxT==8TjzXw)$xET`kzv94gLtPr2I8=_OSz~<~# z$ygyzhS2n!n`ty1t$NY+sDG1dCs4Q*#yM0I*pEhu%m9y# zFFntIJ*A|mDzfvwR#{GK3{KiZUym$>2$Efc;Ny00jg(y*Q5be_LgcGICIETv3(5bk zbtO&Ra?*LF+s7@$)eNe8xj!kBrH`ypREl@cl}}c*Jbep%=Q~~$?gEAoSS6Tq+)gD? zG*0w|wF7;RYDdE@HQw zp_An{wuN#*A1(^JyhOc$yluy!Uej)K8o-IFCI~n;JrzBb({Y9Hvy`Ug*S zPql1(_FuK7f&9A?VLa*V@3aQH1=BbmLz#!)&zgu4MIo7DWZ~-`t&biO<0t05H%f`)1=uL8}W|0CP4uw=?K)C~y#kP?> zC7lt)tMp^ZM8e3w>p-0WZKBe2Or*@EjF=e|PH0L?O%x7Y(xmoPbzp>DeNm%UzdY{p znu7$%QXE$`*AqQ7^*1e2HP93Ex{5J{D>2$*873MJi0E{Vb`UDEq=q9o#Qxf@BJdmT zj?w%=D4A24HpN`=N%fN3IQs@CL~s*l(9M!zc+_M>rlfA;BS-}UIL^axBnd)9?M5cr z6^hvC;zq&JGsIfj8d9^jqcoA2RCUsjdcQM9Yz<&gXrMjCwml>hq61quTjZt(MbG9o>~mRDyKR)cb=@>-&a8k=JiBjtN|FYY=vW( zTh7)@*m?$?CXBXDE44c}MGuk?0d5)(O*~x^92T~Z!j@B4sz@LEO!s7zjDYnH7*6V- zM{LbzTSYhESy~)KlOlI3Fi<~sey-lo0i^kf@#{(6qXhD zvXaEURClZ$P#t;Y?LP*c$3$EuFL0U;Kx1szZiWl&He$(&p>sOyLXzy>-_lax9++E| zx@E^53P3P%fl5f6)0RJ8wnQU|s0}r(m1w#?sIz9rndwr3GG%hR^?9u@#gv3YWMZ0{ z!w5hq3w7`DeadXd`_2gC>H19StUmG!3az)Vs&U>DLbsoee#f9m*cT2KjQA*!DevJRa*MKmBMNUFLRj8-5ts`%Fkj|UhP!T}c6fu)g zrw=j018;GlM@)7q^Hv-F551B#1qIZs5GBOxWH?><92v?mxL5bR=G4eRD*nTNvRP;GDw47N<9czJsdR=dgtacegAP zu$77f^AE~#Mm(rE`VQeZLHyMMMd#u&FAgZ)&gwMY!|L0=_O+4#{nyWjXijxGg}pHeupc8g`eR| z?LF9PJiq(tdW5vSV_EjD)G!)QLYlSswUQtOvq3gHKSGK);=XY3GGUaMo=CH$UHqX;J%}p>VGh z(|?GNW(GCG)Q*X~6QN+kx}U0#>s)R>D!`xeWD-$PzZPwwb75$(9jpm&jBx4^K3mGV zFMLv$>eDCvcfUSxElQjn9{pXPLa>@k>(cSOCNS@^8c;E|eV|f8RPIzTkl_iSAmd+j z&l)3QBv2DjH7gL3L>SQ*%yyxaM}7a6+19)az^V^g7%t-Es#u+q3ZU{joW!xqBrRy^ zQy&z;z#x|l9I>KMRuFx(0xBXnVB=5&;0oATLX+uyWSYD`DV@rzP*qPdlM{O)))_~0 z3&-5>5=kL0Dlm4@H?JgaIc<23YMaevqQW^9I_L6@*rD;DBqmu}U)!aH$Q#9v+;s=c zQf=5>&N*}{c6fY*d%@VMh$^Z_9v)U#t#dEIp~qnxo)I5vU5DCHf1JKdnA(63G%Ovu zL~{?v_CZZ9>QTI}jE(x(J4$e^HSBx(nQ>Z^2mM^MM$gl$A1R$Xou_r=e;?aR7#4E=(10S3YXMiP_;R_7-O5MWSmTt0y6kZZE*AYfD%pzc{_ zr!`fv$?Kz6$wnl|o;F>R-nV~g*J>EE#w@1=>F?&eq>2pZio?2c(%gL4dmTi2AH*ST z=FK;%71YSh;6+;Dq@wb-3mv=ZTybf2pXMso*5jzu@%vp?9oq?@F_knoFLZ2#f^jjv z#=m~LD%gZt)70WPfZuVRUe<$+FXCJ<+akB;_`U_@2}7Ulb@26_4d^n1lLpRMXbIC) zp~lnR_S$xpq6}=MjX?SkP_zKrEN=Ens;dddK87sU@xMQhGYPB))1Ye>otH+~pkLTh zq4VQ}&>`<$wG>73@Y%kKW>c`cE%Ud9><3bxs-=e6xk&aiX;vv^_*xs`m&xf+w@AOB z)?g|9e8Eezqp>k7C_N}rMqn~B7bIb>qN>;qpCaV|xx1!nx7k7@V~8LQGE9y+LmS)v z>7l`WAItUoOi<#Z(z!hTsV>X7lTAw9YyuT{kSJ;zIWU191+>2|QUwx_h-|@)l8w@h zvJURaO>uk%Vfj?$)k^GO^p6_F@LD{%r26Y0`;v@vJb?bs`8Y_#E;y~MeLL8=6mWyV z8FKYtqoZD01*q4(CKzUxZieHPYP2jxJgmMXupRq$L6hE0PIb@a9cVFOTg4@#El?DK zCc=&2$f7JEo*&@>NW_bskLX8($m!R`F7ag$mYk3ouwiw200{C}9(4;AJ7XC_h8+V; zVZ^r8$MGeKV+X>rVHc|pw`x?${GyWbqX)ginmLSR2a>_LzAjOKrNO^6(TY7L_y{=| zO|CI8lND%s3ZRn|$~`AX0#H0r*=)C$xDVrX$vwEo-WGPMG~vPT%d;7H^)!!ScWhR- zGi=iSmA4_Wb(VTlSbHl`=b!Zkp$>iNyNNj8dUhmf!L;!(YG_*T8-62+-E^=Q;{3qkcFAjk zad7W+lFXkhJ>`=XnJ!1|*yYIjf{_l8eN<-Uu_Qcd0&!xg)^rv@NwI@jPDc%NNHv{Lu_-sGwWQz~8mn0rQku*=#x$D|#^ z6~=<6&v!9wxb(a+pDJ4&eE1f&FVMywoIhJ zB?~ul?=(f`!Glu6bS1GXBCDFL}-On`Hi6%{ryxpTo!Yeq)d21uqW^7^@R2RPdqD zA3eZ<2lvhR%qYs5bnjRM$;cZ9!Fi_IOy$PYM zdvu|pEju&`x8Vw@X%c0}W=Ba|^&Wy+Pq?=lgKg*n>thUyBEGtVi(xuYu57q~ifgQU)5ua-s^$3e!!@Et@e6n9;?EP}ef|?(W!g;t|7I`M( zuGYJ(%ys3?5E;izs*6ms7i!biKMBd0!!H)j#5bnx^g1$wpy*h5N)bDWBZ}2&F~^)k?9gP?W$8G_r7gipNSh zp~qyiwI}1paYx_6*4a-tpl$?|_VO9v&LyqZ>(u3g_8RP^gY#*(cK5r;caHCN`fOY` zSLqaA{f9UvbPp}c^(rR;&0@PU6LE^wMgQv-U-+m9I@*X|_A*n1Y$V?6wg5yR;4CL) zSPOQ5+G209*dnB!ITt0S5sYTiy+Q?3X=1x+%)_wNh{vd2D$*QDrYmcEdJHD~Q4d4#97P@2B+jV54U!|kC#3?H8oTWZRj zn3RN&FqSNnsP~ZoJ0lJQrFbD+QtNpYt2v*N3g?CY0F(=IRH;>DZ|SEc=g0D)eM5WI z)!W=g*(=#JUi@&m={GOEc8i+R2T^(=;{Q%iInQ2%@V49=c=$ORnKpYOLTs)xI|Dlca7+ z)o^Y4XcYDBF|OWJJ81itv>lr2;SRax;;bQo-;MD{>Cka4xEm23B(&WFjrvoqQ9L#R z)n2eCKii3qm?`?0L?9AuuA1|ZV7e75+<>k3oZ>qCkh1}pSvo>!ZwE9LFJX85> zP5Yc{El-b~DEii^WBtCKWdAlJKs7FqQJ+|0`P1gLG<3hw*&!G@t|VR)IKW36OG}tP)Mdg+L>cHpv}-9-gOhh20By4i$zvUO3`i&sIc7xX@B`GFj&_9fK_dXr8& zX{qJ+jaZV@Vlr=es}+eaj=a(Vo z6H4cR5UR~`dgJ8h(^&ywT~0$zKd(zETYKWBE)xWu>e-e&w~uDJx)R9W3@u{lD$9$G z#UKMQO?R8Ma}J2{N<)Vv*-PI>orIa^Y&_zjuh)`4bV*`0$MDye`{u?2QUb!bUM z(4fN{8E8*o!+R^EAl_z**VjG@-FM57d=SwOkwqu-NCYHnGpLax<}X*u6hk%zJ%OJP z;{+pxJl7>;G!H@(3HF!8rYX{S)xR8IAjYr|$3{3!vg2BdcSMv3DI};;D#?i`d>C+Q|P!w&?|s%XEOV8lGzXo zLO(a)|0E)*OQn#O!8(^gHc7S_u|#2IY0xnKp$eLfbge1^c7-3{n#x>)I+Hj6zKlpG z;->r6w_$HF+ML0jFo|V@rw(gM^x?Fr_WSeeKOb%U(`9{lq{qnq4^wGTAJG<}HjPs) z3iy!=ZvV#%2HPqHd?ixvs}?Z9Gl5{p!nA}r2&s2AzkQnu-5t0BmUj!K@iw5)V1NtV zk%0F>@Q)YUMs8ur1b({QptE4FS!u=I-&+M&x8`kg%siM;aT44r7;^Y|u=5#Ygss3K z-SSZjScRw#ha8N(lD^7B5m1q*Q*TIL>pyrBAKzBVBgE{Ft}`q6`uCsezgu>y94FR% ztp99*`VZ6E9o0^QQWa=a_Lp^v)vx-`m+k$(l=~06(JiZBF9VkIn80b6eQ{GfP+d5! zW`s*th8Eb7i!P!S4=oDbyCXlM;FKK`yU{R75gkquf&jdO!SCL^ZHX(%;|JkS4!qgpT zJ`KXa_!F)%4LK7_O(Y4=!1*gh1EK`@6oc}*5{T|%Z%wx9)-EJ{5h$Z7sw7K76TvOU zjT<{r&bUzH*CdMMR0Ql9D?jdCDwG^_#U#l8;w@*LpX(!hV3UpY5AbrGK5Pz;l+?dq|b`8!U_Ki3-EUyfq*#4&ZN>C{b!q#n3{q5*kgJ z-im@=|1W4zJM|zxXs%p6G10RR7ll9D6bDrcY7&9+Z3T9fq@&<~7oa#l(L7*&6gqnU z5~IVNlFWPjI3Q!@(9_#Q0e=U%za8dr>c8QUN&}DJWPmuN$kE|=!Q_r`T$Uk)$zO?# zL3soB@I(M{S<5T~PU;d^3>W zPcH$2#bV&uGiB?h!}9Q+DC10kHRZ%Al!8Ulrct--NrCX$4vDVuTl=BVP2mK7-pdk^ ziO7F3^R8)YWJzL}tjM1o~RIzV6?KQAxE0JU|HNG_;2 zlZ>tZDQKTItQa|>ZRj}!Wd<3KRm;}+279(p@e;fS-WJ%jkR!qJAtN6NuThLiO2m(2 zZoVRZ(`Uu~6$RtH8Ey)n#x*#Py~3=p5RCJmlY<`!y`yNUUa5M*x=O90$*DFZVE5tj zzAD1YwTNBjg5j3ajkHQFd2MN}8+;f9+%vt7j>xh@2mi4PfQv(JfhzO;w=GHg*#ynDXIY-z|SrV_kQHIb};b9!MiX)|5upDEKy z3#0>u*m(urTbV~$u(9M4+gN|~Y&<@UEsL~cdUxWxG%1;V%6ml1nKvCrHqLJo7PJ$1h2E$|c zKp#d;w%F=nz(yc4!2DRoZh!IGn}6+Pf%l1jc%CQB^h2TVQBEA@5pn13g=!lS`@EkX z8L);Y4~k~R&Gv!{tpqQ9Mq3LGXRddz!}s{4^fTkcI{nM?c7-{7l?D5S>B+}N?xX1= zD;al>$b~CTcrys}V*s#@2C8l`Bia*c5z87XzmlPdUL{=+3yTo`WI_-7BHgW6uhTq6 zP}cT~FBXuBK;8^d6JgCl>p8I#dB;>PH}3ul&PX92)Y_~s4@Xu1!PCZHkGrS;Q7pRx zws3DoU&RReTRR+yK2(i8)_l6qMc{rMNhe-mYy!uheXb&XW;!u>9jSj1or!XW)caik z@#ClFpa^dPY}nvErOPeKPC;$%yR*{>L(l!mSunSJ>$&dk@KWxgZ`<}9 zQ105@$FQ}o-y&*m0S0ysf99cX-}S`1clR*1Dhku#j=A~+fU;QGV8IW6cT*ev!1Owd zwfmktP%6&`qU@Xy1?i9%8$5U-q@FOy$gPl$kSEZDASBbytzsCBMUVl@r3$GW7lDdI zm~rg9R#yjUsCMxg%MnjSjeSa~{QU{j^@kV>Qnt4eRhwQmG{&ijqFsig3RRdlh;M9o z(WUI{wB%xUr?TsX5+4-oN>HYXO5B>4n^eSp>4wPjd#Cf!TC$^H)M`;a0tWrAVzPOV zQcmeu$5M*O(GHdjMLhvAjMqp{e6g4mT8yu3GHGQeLn{kQV6-*pugx|E7aw$RGT6kTNnH;7&0 z5pQiyTB1&KOkKku1?e%7WZ79V*LxDwTbE&*sLs9|!w!8<(??~CGn}0fsb49A%mw0G2`%*0R5N$NEpdT4NHN7gSgjdi zHnp#-K?sKJJd~T~IBFF9@Sk;HNLU-FBbbf*Fqw34Cyx zmaB82V!y(TB2#y5e;%?#Az-NZjWrT7mn^AiRPh-K!IF^Y+jk*TB41+dHeD|5^n<{V z)SFt&fpgRl4#W|!`Gy`>%kB@9aYsiv^HO5$#@9_6khFst^k1J$j3e`9H1Z;d6#Fiy z_~YcI(vWYzi;DX0+ckUX3(ALVcRO*?a8D(W<4>E1Hn977NK*ce4Wi2T?x&0Cbct3}ekBohaQMi@!LOJk z5)!~lqgHRF*V5t`_#Oi<^(HN>hbCxmFJvaX3l$tW@#R@kk6O7TAK_X<@%EE8#A%U9 zRS$9p2c6m_7RwQFiyh;P<(jNy&$E)#-&fm%y!`vqN8Xc+AZ;T>b@z!$|JYNVefkVB z@zTX3#p1By%Hl`J^}7sR-D8~5ey$mM3;tmda^>VlEoNV5_Xmj)AVc39J9vafIh-iXCcU&h!J2-z0OC`|1 z)5pAs*ziBGAe8b~E0V-qt14#}I9((GUBjejZ~=d*SKUsBY&MvrRypLGTUEF9V4F_T z+sL znL}gKXd6UTYv$5F!Z@=P$i9zmrksE4Zu{TX8H_0gddI${sF0=N+J#!lrk0c$KdEfi~m&C zjs27zFvGR&mOr^v4|XQU{}Xo*Vf$lO9*~c~npxyZ7*eVgUCHpND-9%In|>H-aaBL* zbfQl`s1nDiS@`k^iq(={Qs%p$%CKs3&~|tVUj)s8v0{`RTCpoXJ0n)dkt=`dqg#ij za5s(z=#1T1Y<3QYYcuKs{EfyoEG8!#%WVswvDym8llZ z>A=5{&YsNI+3?l4>N%zFeeHD5l7iLSoIq|>LBM1{FXjA^qZzp5^ZkjjXP78#)H>AF zCi_G4gGODzz^CgE{K&&=%JBJ}u5v=x8-~MBb6GR*{czf8;4?Gebo%sZ5rqhy?2W#g zFh+p*sY)$N%P!Bh``7C7zr2FROgjhJT2>GHe{9a54FC#Kb;;!mlTBB7)xxA)zM0Q2 z7U_0wz(I9N)Ptg|EOI`|d0Ca8i!8!N)ww1mN{GzPjsV58@|8%T3m#-SI625ThbTAf zg?yBC8_8Y1c!G!e>})8Fi9R%&+Gb=8Ku{Xq*-q+x6BA#QS|?Q&IB7JV^Y+*z_s**n z35gU!#0CFru__23#dxhGRnBB9@hqOk*m}d!L-drGa#6C^g|--3!D)ui^KFZW0|FGP zix-)lE&_5Z`N}MQXP3U=A^SF>R={>pxzCm(tu1=1+LVhd!g;Bcc|6KnJTG5?>?mzm z@Cw`Sumu2)5kYs5^_8pMLW)ox;I8F^7YzSS# zW+lNAAU-=Rd`(6mtZVF$&I+$LO-E@g=BBQwuk2m?L+07X=fgQNa~8ckJQsy?fQ-wL8w) zLA;v&XzmBc182a_fW@K`=Z3W`wKc6Y zJ%PF@urQFY0!Acx5h{267Wh{+#SCAqtsaZqeu(F zoH{VF@}%47@|;XNL#nwNV%W*ka+=26POKUkpw@@liw~?i;WN}UUu%D9Lumf;L7$T= z2fmlSFUbPUQ7eu7mO`^uR*}+Ld58pFa9AqHG`x@iS=-Y{Rh5J&yyi0y_!FlKew!mpX*4la?vcyb@A2QuDPyHsSU?3^)RN* zq@6udtgz*1kkkPa{?I4CB|O`@t@w5lo)Kfi!`ZpuJp3xU9|_T9IR!8Mo1a{7(k8qG z`Rg|aER_Lz-BAl1i98y0K$Ta{l|ArtV68ST`Gf2eih|ol_YLkwU-(gRf}O*SNj{KV z@`U@f;!fVDj#m)S3~~n(Re8b?XK0i&nvjUp(}+%XBK24aio)9q^vteKB7(WratdyL ziBru2`f9ABFepnxK~`&Rjk*VZY;Cr+p&vA&DBR*xWdxXIM&`)Kyh4Q*5xy!&eo&Tenp@aNaZ zK5ZtWmHmWmHJaLd9)AU|3l-<~U{Vr;g*VsiJj)px#CK zo}kBKV32OsVOef<+uA!apr_w!8)sttoj5Jyk>q>T9#ZBz09*IM9lT$xUvo=GwAyE@ zpXvog1#5hC_Y$Fh{!i1=D%~mrr&YPC+%k4B7N2On$8k{H-K~cZ*xaQYRh}wO@Z7VC zQI#FLxrA6qK2KG-|CY9W!BbTTHQObdbvh(Tnw0Lr>HKTC3vb5iJd<_kLe+HB#uQIc zf;fS5oRUe-eD#2F?fjkR>Zqr8k)o_rX~y!j=p`WVx}Q?n8rvEY0?!Chx+Q)n51tqY zYJPGwWV`X+Sn}p}(8nTj*`T8Ht zjgdT9?v*<~wxB@M&agCLTdBL6UP8~I20MP<_VH(NpZ)=PsCPb|s51Xp0WHjlV)v8< zYGID9BCn~x8o4`-sZK;PBq>Z^kFjocwmApkLb&*8%-?n|&C=f`F3DYG@BryWe)E59 z#r_l4YF!UIJU_0t95|pyA8Id{zuk)H4=1-m6~q4r0%eG8z)m=dPOtEh~rWojKGCN`eu zDT~w+TJs$c8Ct=Trvt!$J6kZzf|4+t$Li2>_*RP0*oi%~s@n_w%hsN?;ZcmJumueb zPJdn^Z2>M=wPnVN6HdLU2;b78pZ#6_^}FjZ2`#`Ztrd#!3C;GbFY-8qbew$iR(9s! z*Y!{J(^s}k>JDWhsLVj(8_c{+=2=}E$$%o zHaBYsq3OE;MO6_crIisy=7yG+IMV%EocY~jbN>4)&U4QOu=?*LNnE`2h$KE>mkJZi)pIS493M92GqRFEC>iTbW}hkF)WE3MsXWz;l46E+)hRki0$~Xi zLzi2@c&aXCgs0Bl{+Wx*pUZ!){KLg@L3CPl2D1dVQP9wS@KMHpMMI?%QC}wtX>{d`TRtF~ogQOHQWJ zi6+OSx|-_f;ttKU#aA3E6~W1)qro~x6A>(AIZTxw)T&t|WYbSXXP9+5b6vF0v15U< zsHlJUU9k7W1op3Y{Tq=3Nw_}IbthY8M5h#Me&Oxth&dx#5MSK+fFjjg6c+k(c{yjz zix(oGj?BmaFEv$pt3Up}Kb>AV8tiZNrAH|jlYW89Fhe|NU!Fss?8$v z(Kn}ErkRDsMa2I4>)f0pJ2a6LnX^p0S`-@`8yUMpw>DC+gWgzCqA0^%#CIx)gAnBaw%H77FO8axy+Xr0RLC`c|YC7Ok z0CCtrA&n?2X0nZiRE-8hQejx=DYzY|xFR3@!0rL3cVj5;OwVrbUR_qyw8N(?)dWIW z%I`;EDNCeg%PF{{2D4rhETGNq*%chLzVnYTAIET$#6$DhOPp}SNWA_l#PCdc(whdTk2Goop zeI==2L#+*^7urDrS_sX=)URwdph2d^xyOf?VVaf0vGujwx0p<{j z;}aEsysWGa;3g!V95^^{AiD$p7KI_cR{qNl_>`rJNLb3z)eM!mbDc`WOk<(}P~M#` z#kkRp)$E+Sr+`3~^r*+cVg>Z){w61KgeK~4rjP)IvS zfpES|cy>8j!MIpM(XXor-QS_5`_w30k(v7*&d16qE3$~NymSmbOf89vUe=6&D_vy;Y0BLr>3+ZbZ*4j;s&%!O4H64+N?aVa!;_x9Xm9fb2jbeeh)nQzz-`2k-d;5JuC6zHpk66*73zP|6Be<(R%oqU z6|%I=o}ed$I${)(N(&?$Z>tx*Osl+0%+7KkN8VJw*6Hc=EE2E%6Tv<{_)1Ljx=pq!o44-UzF5%uF;rqhb^9GCw+}@Y1vt={h;J z{~cTk6Mj9$3nROoNv_ao>@RsmuGTKnxv0%YGt=A`&zt7XU3dGV^XXlZp7{ZxZ*MoI zT^hLJ$ezKb9`4DIY~uZ{c@00&fJXHoP2@71p&{Y}4Od%1S#5p`v9XHH*^OUd-xH$@ zz?X%jB&K!Z8zUMcM}{LBBP{q43^I5S~o)>SwYxcrSbhLNN7T7)X zHJ;K2XwY*}3&k{FT~1)-2FLIn;P2=WW9wdndY%qZpu#6A&YI^;*}9j&1&~@>Fs&w= z`}2B^K`jBTFH+FtqwYw*x;D4{u@rjQk#wDlB7Nnb8JZqlvr2V`g%%8i=P-4Pa%?u(eduA_j9$d{MK= z01%O(O)eyFl1n9uK|}z}rT%iFM%*GPZL!13EJ!o)L+sJ7ZK~6SFBzMfi_`!B223dc z0MH~pz;fFQM6hq2X` z?LFQkOtt%I;LV)kvXW!hw!a)4Dv3O>I74x)r|su(-G;^G5j6@I@SR-7W34dZIEeW7RwR}QwTn64h;3@%Z? zKIm>PU3e$iTKW=$UtnPl`is7PEmL4-+xmyY+aI7_=>Kq4bY%OPhfe$txUNcm*b^6> zX|C&1omypnUIVd7QZ|o_%p{{zqg*R3i!CK_pod?7_v%JKYP(A@exJI;7D za;fdc3jcsSV^8B=L3FV}r>ifDjxMTSXEhW@i?Y3PfEjznQ)=|YI50zJT8_x7os4L} zTZY7Ng@;JDebFT-rijVrMLHN9!wJIo=>`-MR{Q;Jx!l$?`1U)qfM>^o7CU!gb51xR zIWcGS_|U6@P&qbz6XQ-cCha?cT`v0r@QHZc;P~X*{AK3kQrJQkuseErVlYgyT?1IVW0GqMj{5E2`C^J2=4is-L?Bz+yyoY(&JYEt$xisFp zS0(ON8u3JwP^eROi&b0s@fvr+WF9w%H_V$a;cYG4QanJIHpwsKkG!77H6geh);3vF z7PznA39j6c&*P2d*l%j~1(s=&TrfFYGw$oxBl#EPrfI^n#ajxu@+9+l!@L|WFNu&m zKAykTuNN`lQ4)zz+oRVzKGq%Y=Vkl7%Ywayn;To7m}D1CBfmYtF2`nzf+?EPpuIH_ zg?_Z^zl-lViwTAheQIxJz2nw`y$ZMdjVJ^HAd=G9?IEFLAk53WP;Y6jrMtV@$@XOT z+GdNsh-|nYfHpj2yGF}bFh2z6?tF#Y-hvj#`i#BPbi0r%fqd|32(LsE{DMS1eb)5e zjMW>MU@ZWKG)kBQ&PB8ZpOm`wsP=}0WO^1Zf)J2^GORl`770Fl7ScV>%-lQ0JQB{Y zn>lmFYn>4MdEC(RMrWI%1)479}9hbyZ}Z%7MQLC@ikecQiRUhHgSDVC?} zbmom_$s50W`O|-mjdR1p!gInc8RzITcItDw`!g!_ujI!uk0f=dyMwY{O7bID3OCB# zvY2ue9teWe8uk?#BzirhfG}m^<#*@VjjRcsSBFVT!mL^gVu?}Ryt_OSkEt!YUf%!< zob^Z>{-wvT$LHa0j6X4<8WvBWqKY08=Gt^~7w$y6kFZwR4??0V6 z)|uqPA{vNJ@AeA~3~`4{id(bfMAvKEoftDPqJA&U{FI>b4m|vvBU#!klMT#G5(KZ` zGwW~~ckt`bXo7&`sHYQwPvf>ZS3j$7zPWxqAOLTX7P<4oJtv;__UQN|S1Q+kFF(QI zC|c`t=2| zOR02O84rB_<4y5(v^Aw)@qvkM%g;dd`zC^68|rQfiOCO<`7U7XhLMH68GGgWE~M7s z;_>4vTbC~%tY>cKYAR+ZSw=iT}UZTS8;g-Wx8pIUCxX z+F*9csoP(8d94@Nhwj0s9E07;G8mbVQ3Mpr9lqC^ z+BBgIzX30t9WuuS1h%-?ExNUh(FnwNx&(S~pp`tX(5pS&&<0rPfwSrR9kzvA(}7xx5rPi9o!qlFG}F6Y&kU z`AKEv3CJidTge?ANH9mjMYX~p>Zqk`%oc9v#=>p$`LR_t&N%8$tF7rL1!SdN(1c2C|4!J29KkqA%2jU_^x#|f%wx5pon~EKc zxiaH7?x%N}p>h12($3P^oKKT6p9k9%nxkC(hdWxsX#L zU41+{ANT$r3y23UpG?`Z1#xG@u}@|C>ST7#hTH+0jZLsl<*ufe%zN}$t2N77lB;%C z(RFTr>8MIkv#sS|5%JO6YUErwRoYryB3Uf;FR7CUX;-FcA3y3RynU6s4#bnEC3z+> zYD$yMxp^fiM5WmZ|ACqYmbe0S0U{{a(G?5)4!2fWHF_0;=sPmeIgb($QYt zvd`hzxEAqstryMX!a(YcxA>+QkWS+0+HBmUaJz&k=o#-X-ZtV{n=IFf53&tCD$i*d zA=?X5ewnz}Y!-Atd=>G~XGeMY+Uw=as=t1ce|K7~^aw!Ac2KNqW(Sj(D_kWW{&D)h ztYQ>pTawg4y0qCna%8EX;>jKTEABS0{!d$NHqLI`b)BCR^Xg@ET2MHJTJ1t_#EdH! z&f@PoJaXgquBM2I9Q$_w`-qIn^~=+>%wW9Pm_N4a^85ml*k|Xb2=9=l8q3}EA!Aj; zmW+#IW5p|iGFH_y%vG|K5I z1vn^4A_L|9g-+{(PgU3SOy3%&J58T!v7~waYyHB7N;q>7w$INmFK;sj?U3|y9B=kLy2f(6d)wBl{;LYm#SlLxB^fVfoMB;G z7;YwN;rtgrYX4}1-wGD0IGxyOGpM%pJ9=%K)-nHUVV0o@WznyE|A*Ccep*$#>3brU zUu)lYd)tHiN;?!5nky^VE>5w zSHz~cxZ6qPuiLeAr&sP@yEkA9f=CW|)m$tqm2i$FN%vSij*vv#R6A(WXtZKXutOV5 zW;wkUaTT(O2aP^_oaAimJS+pKrLzMRY17k}13aKWox9N(hY0)&21g~JZU;#nesz^A zh&*$SBSe!>zPaP$2s6x>?j`COatwV&2k%c3Fl{^DU`Rg!U^*4@X)ih-x_CtAqZloYJyvVv&u?rDJGHyyJ8!=-)UiD6eS3(~%1;FyK$F$>XrYTP9G ze;T%_L97oYri|7_MlrUWqP_5~o>Gd`qU8HzG=KX}jY^lci-OmRVdmxoCA066{eS;m znj)jiX}NQE>`aiJyDdy7kyf@7)ZD_`QV|v4(SHfW3xd4VxyHc{TCuEZsTaneO2j#GPSQmVL8C2=#KyZkqj+r>Vqrr(jz)_NUTjkzqk}W z^#DCp+lF=I_6`;v+-%r&yV!ebug8+iAZAfe#x+GE`rf7Rl?SNu*tVxf>dPO#^`<)7 z2fTe)U;e|ZT||EMA}SAEV)pWus&%?rp0jD4Uwy*eJe^E*^Hvl(D`61;Sn!7k=p+wa ztpfo5jJNhU_F`ycU}WV9x3Q)it-V1uKLXMM zh7)&Pr1SW@+A2mc{c^c);UnKdoqa$1O2w`={yeG6-4pjc?|W1xobIj}A8!9za`kzS zm+yCdCQX3+cTIRLx1darbgkB1%JTL%%~{doxJx|=t| z>m96~(@HM%-P2y)t-aUUw!Nk+e4q70>ukmyRcSP%h#`g0+qcr?0{>%G5?oGubq+!-}ZHSY^AT* z&a!(aS8U_XPkv@Ygbu@v8;fPveuLRh`{c3X7G-6B7!Y!hN=c_UbOV=_1;MtJP+ zqZSLXKcR{D`0{npkt3W3dxG-Aq<(qT`sX;2UwDN4pzB9=pJ8{F=r^(u&t&|_?&_+N z#>YvoN#YZvRU0=dSJaVT2WNrLo)fsp5a;^ln{2M9?BuVavqv}&b_W%NhWO=G7e2$K zzi6M3Q?bi)Wm0)`-jane$3N|=u-IK+a^+M^AzmCa1}~1CcjaoS+=@OiCHjqAw^6)l zQzgs9tLw*D+117R>P87Z?Ke@@?yQsz+%4!PH`d`Jo1j+c>c?U>+4@DkP3SG$DsYr> zeGYs7&s6B-NWm)-4H<`h_^kt|pw_($w-sm58I+;Z3=rWXxKGuvDKA36^`M_wFfG zbLL2-bLJ>jckf9h_wK5wUn(Ih$rp+DX-p=yd6GuGOKohzV=?>vhR2Xyx-LF^g!Aj3 zpn_1TpRo#lhQDnIfm?K#J(?S7)u_W8=R80@ImEhdzk35axu?xPVC!mb@}9jO`f=P{ zZ=hDPBu*=df9LF>h2JN3n^ET<@2}H!&tOa7)*wC=Xl)w9C)^O|hJU}w8mI~268*SS zMFFMZsk#WGgsZ6oI(N>-3$w>HE@YT9GsaXUkaJ5})OZz1VP{?lfrM$WA%>2IR^Ylc z^(bEVGo9k?H@Rvxc?IsoF$UN*1Az-!a-zD;?u;&_kZLTm*u&hoFb;*=`pDFX(3BHE zb5W)BT_8RS4`<}9$K4BZZg8=B4+6D_xUcA8(O|wsoPO036u(fKh#)KlHvfZKgAls|mKr#jj!p<+=hyri=3>K;I`X*?YgA=bL`(1f97h-Lf|@NM}@5)#YnxV*}B zNTihvst5!7n#irtWvGn|wLzE2i^!RLax3}sfQI#%2KxOw3(SH?yF1FfH`t-=iCY`^ zG;NVtdnc}_NNd#EK-`_jS&M`QI@DL3{yNf3#Z>`uqlzct-QkIO{?Al5@F6GeoxHCl z5rwh7l47KgTEuOhikAmQJ%0@~MYB?cnsn`wdTwtLx@e^mGI!5~PdvkuRG}I*`PvEn z+-*rH@RmW{lcl@_UcR>|jnLR(<85SaWp3nIdcb&7Mv9)*7nCoo3Z(H`ynE2hk1IcL zoyN>zY@mnuMQjcaGx#j`_GXrGC;DlfC%VYlHFT>-I+w{(xF=8eESL_5y2(&CqzH(p z=9W79pbCnFtbP8=0|fpX1T$0ZL#al6q5VX-P|knrmcj-j&)2;XZ#2b6{`!WDJCuey z1ikq+A;EZobhUEBOk)OJU?57>kK;u?()@9VbNvDPY*IGS zCS!D!`qJsRVb0AvhS;MAn*hM?al#3H|Et4RL8vUU^n8|xtUU987PVA^nsV)uGV2O7 z4AmgE%)|pvi_L0N6@69!k@h}Y1Zx08Aa1Llox{L}5g1rj2l`&AZ7}2wX$<~<2b)H? zSO6ZFle^9RpX6i322bXR6?LAl@ZHeC|hZ*VgJp zrVizl`JMPu0|581q*}ZW3jkqp)yZl2BTCSg($lb`-cj8R2L=`V+ksWbJ&MGqlTnkx zI=Ng}KWSdp5V1BTIbvNso&^r@ohlI5!}$4y8aTGFK(i+cV&Pb-;d;6DsBjqOnxWUe z2H4Qo0WSIC`=Z#M%;{%J`!J@UUkGKp(}LqbJU(~*?i_NEFq?d|DTHUiIZSiUe`7+8 zPxIpH!+g*IJP!W3UbPU{@w~7I+he9#*20LS@QxxwM|e_Fd$<{n=t%MkvpFOA7#xEowO$lpvuX2mVmE0 zpL1r7`t}Fwn}AKjP})&3x%vzVy=o8v0l<}H#W^uKy*+u^dPlL+`T~)mf)&ZnodTvK zh7N|D^hi;7MkA5S7kbFZZxscjYAM`S=v3qgFsv2ZtjNicw|G?6>}m6CCNF~Y!pTK% z@qa`{xueI94RJ>Ea-DtJR$V;)+~K=|ej%1u|H7Y|2>rS-x2wxABP_5^8Hy<;^a?<8 zyS#cRsF5U?DpM45`68p(nu^!W97XBwmB4G7KjCHYip9vZl_W%6;}v+>OKR=++VMCW z0Hy+f*a%lI4681OEAV>Z<3+5gjCXE81jRYdn5xyVsXh8~Sucwg0Dx4q?%4BBImbS( z8##=3>~-YFt>tv;idr-u*SyXRYrkmn4nI?qmU(p5bhB^hR@WlQ`>{>~&|g?@v3xNW zjvl%f0vdAH6&13Zv}%(F0=Z-ncQi$EIm@$NHWgiL)}_{)qiL09{{Xn05XD!(@N+-S zj6Ru0UPRW!A+G#*PM4?cFPSu&~qoeE1sj23Sn5riknE=35%-nR$KB5p1irN|g z2&E(KM@>K=tSMF{#Q!U%d%cq1OeQn^{F9h}K4K>O`7x6x-z0hYd+oG>vA>?asxBGU zW5z$yq@#CkQ+ItvS7{!_qAIUmQ**Z1rOYu134FtDtqIBV)HXDMf4t)J3B4TZ#}iLMdAc)*rR~Audi0*d1M%|d>82lj zmWC&*OlC$rb}?1Xcs?WZz%5F^C~JIg<1Coe_*jMtUD*%iEY+e4mdKwYZ_a`CUqOXr z-XD`N|qHC&}D5(8W>|pb>2tq^Rw4t1opwf9gdD-C^VB0v)(I>Pn zC*==@AQB}h%`WvqGPW%n9!}o$StOS+y`-+5ox9+Pyb*&$Db)@T16V0&k3|pb z_LOnD3e5Tmdv96i4a&~j9Ou=}oZFqEjl{yn{ZRHYEvg{QNo!8Ms3;h-71_qp(~g@= z6sLw%ZcCbaQcU$EN{KkME1$HybC)NznNHXh85UCJ3?~l%E!iMU2TIPqS^j2n#bkcI zZScnL%^SfR_`|P-x+k2U4+jMmIQ-Kj@*iS%@UEd&TDR?SKjX2;G@i-Ky4TKJs zJw6bTDtKDljBc<-peN(S8F(Iq#T;~<*k|aq>!2$zTzX#ut{xw*{mpHyhUChqwKP4t zT~IJDgty=LiqBI+r?gQI*oO@-DyN}cb0Q44ji0%>gQ4*4_<(^Y5U5A~Hu&&{X0yBW zfiM8ojF>-Cx8JP`xHc|1*wdy6%+FuCn3;$quM1088Y08Oq6{k>(^ zea$FkWE85f<>u_?QYCU!BvOf7@;QF91*M3JL=`pn#jjc5|A}ieR`^>4hJMUN8=y|Q zf!+y$Q??W5x!&dcU2S={u>Jp4_!d3#DXRGL!Y}*6y34(}=d)h-K`JEj5$^V|*?<`` ztcHf7}tFtTl=?a2n>d@X)80nuR41}DNIdUjb!0udGpI6go+60KuiMw zK%y#OsTu$~1xD{)(1|6q+PNL)Giqt%?BD=W#@}Vj@6wq3N|GCmWaTqUq8y*QbHFLl zpc|o5M|1{K;^sR?9i5u`e()?mn>;=7B;&C;bulsvH>Kh>xUmT@`;GVE=UC#L`z8d# zcHZ7<9oE^PwyEzPzY}$6f#(l>vGdQJb2L78XQPu=fcg*Xgj!{k-jd`E}njOkgK2PsMFQ z@W|nDzu^+ekL>AQ=u9!2RzpP6c>)Ay3EbO0~uiiqBd}X1>Sq=ig{az+7)XQ;Lrm8jF8A-A3ClMYH073A7XhINT zEoxP9LmktH@nq&5t3MSem`KZAzuihlASOEc*80krY3ZMK>{#1Jb3-5&J^p1U1(`c$ z=UW$6gK>HmsuuMNKQh3+K=@oUbBF0xshK2nh{O@NUy3@~BXN1VbG_r9b#HZ1y0`20 zIIhqB8Hu{w@_)KG{bN1_OJAQuy_D0^D0>(PPiL~&dNxXi^5%~Cf8+E%qkWvR@nIgd=#N{eqk&;H`KH}xW7XBtXQyT zgB#>FX*6(1x*qL)V|Xuv!SGY3@W{wWJ*JvntgC13O^(<--i0PA68%-YI7fb)k<`h< zA5b~Y;NP8rKif6ue?>2TS+jQet6kZOiC2wltG1Roaxh~C(nsiMA8m00)YgZS&GFc% z8uD(`Mur(3o;6N{IQ3ADlfG8(H5vXWG!L^X;a8*gtGH=Pj79?*1Zd;+@62f+$l8hiY`51h~Zma?iqd_|PA>HC8Xp{YQn zReM8TeXnZ7UHN1xSN-Tv)=>DOIDr3g-qJFMrxj%YnU2`lpbE+b*XCn?PQLYC=6cMj zi{ey6x&Iu03a8iNg%|L+_0W^-{06xoLpEg??vE0(;lr__$VnZt|9d-j3Z#aHZ=9Z@ zXh}}not!7=I#aq15nNxUxDL6x4LiI3-*p9C(X1ON z`m7xxt)ldYoi@q0>ngHR+c1q6F4fG=#s=C3y$8kORe1gr;L| zVC3!&*DKX;Cg1Rvxw?k3U$R48smc#lcZ96wOmpPrnwT*w-;)AO zC4#|Ke)oQZR7ku`eM6;w)yk~rt;CD2cc0t@Q<~&Of}Up8H*({(Eg(@jwPp6=G*oI+ zn11S@&3J0$HL*~z!Vxkmn)YNUM%QayTd66>DOdUp$PUW1j5fj$fpAJI@4H>jQ=p?O zb!x$Hi0tB>T2cW+XYcTSgLbK0vYY?WijYS>m?)$<~&x8lb8EdfW9{LyHqky$TSHNfdH_F@J)XHFKRD}v@8nJ z&LjnAhnMEmT8a$HfU|DO^;IfGMYj+XC;o9r{m4=xFkY@B6Vwc-9hu*jmVOUU0UKO9 z*#{K}4CPusAg+qw$HtSkzu2Wg?tmjs?zi}r(oztUS?dH&T+SUK@@{qh6TD$QVyxH* z!!zQbGGg+!7s;pD^7_eC2I*-Ol@)}s*s-64TZpqZF;YfMs;li{n)A`MaI_^zAs8fzP=m5`eG}5cLwEh3GBZED4ssL%i(UWqfr<}5T>sUX8}HuG>e+MiU+xu&xiANSd_S83C zQAg*gIJK)KXFbWyK>g(@-t8K`@Xzt7!>*|HzY{wdnjoip-rcor{R2WPBa?OoS zc=dSO#%RKW z#>iSCRJikTi`#NtyVDP;dV5(W_7g4sj>HCiv!2_kT)5gjops^(QM|O^d$*)ow`NR( zuY%@MR9p9!uC*I5MQdCyxA-}D6GOYdW@MOk^k&`B-+%7qHBZdGOz%Bc?qLMdn&3j^ zOC+OxKyiim!FciLp0yjsi|V|I(T0%m8EkE5l`Mh)4zF01;lg7EhGb>LUSF|dy{mn~ zu8lRn#>Qra1TuNNSe4k11{@xpsS~H0=>bF6Sc%@N>MrhS+;ftEcMa1*3~=~;2{+6I z!&wR1Sk^v_C%N7SAWe-6W#Yx7dmHy$sH^ktB-DojKMPE$zI1@NMxxU2On~cYH4myc zb@pgoxqMWUX#wnZF{mUPAfyomRh>p`CbXW=3hRpQZCJZ^t#6=~K^aA) zGViw!S1QLiWAKb1{GmS>@d{91A~MJqG;!~IG4kEI_;}+Kp2-?m0#sZuBchWOTq&w! zo|#tv@s|2-8rqgsYNEe5Oo|F{cZh%*ZCpvez7Db!{a@PYoJAh)aO+1$c?Oq=r z{4Oqvr_rGtVGxufAOw6j7PakYHd$DdoJp=diYSA;>L3JxRwuy#Mw$8?E*8fZ8HYwO z|2D~1!-9I)z`Q0+v`Lm~_ihXz#bj&3)ghJ-@)0c0+f1qr$8j*Qs~XhS;)6~p07wlL zvW=qj1_&T5U2WJZ#+t+~0%e8q9~bOb|OepeeX?w*89ey2J+UjI+~-SwNtUL zM7$+jN|1OXHS&^_H)4ypbgz=wN^^A7*KMWL7a8BZb{ar00A&gQklg&a@9#(XFF1eu z{N-)oC&l9nm;ItPbA&4X>tEP>?AZOtFPcy^Z{EXJ_g4M*j`h%cTInA9J&|x)&FG=G zssjV_g926R4`1g`e^9s5dpy)*Y$=PaCq!m@8@AEgMypcFGT}0P+ej}KHSBEYrgvxm zeFBGh61xb z;NIv|O(6^@_I`cftDHLfmMVK(_yqIJw<`bnO9gI<)>as9ZIhFV8Qggj-ai%|k#5*> z!^7k3L0tO3ic3+<${^WfkmGXoMGxj*08&sY89_~XE+}VAOUa9#qf*|xCvj7h#~v=6 z4l~u%pofiLv!6(7v?TIkcwN%?+xSyAnVyd{b(nV0Wig(dbSQfaZicn!0lc!q6Fc*9 z?{mf~bgNz=Ty7V@W1P|YhKLWgf)Ou>>2X^F=sB3d1E78MM!9a+8llz*x&g-tG#&#w zZNUe0^5>>CkO3%?3h5-#iJQj?z#ariR9_;k6B~MO1oSZ1lY7f*=Z+FPm3vFYnXJL zyn_y@_)7GIJ3lg=saLa(x39R!L~RzNq?rHuT_E`QyLn$W!BVs&ju_l85cKLV(zcRy>k`Z?6fPnkL<7K|yBi!=@%-P~suYjRa|KlPzbOS&Vwv&8JYHa42-B zHhhxM+{Z^<&tUlsGmh6f+*x8S`y8Q_PLXr`Jxa%b@Uu|X-V1Za$62C}DgKlGuIM*? zVggl9KKl56=u-to`}joj9v{E)^?CZC?HKUwNbveSZ|J_lU|;&1J?n#oyElBL)BIdC z`q$mNg?DRXva(|8Vlsyo^vB5jY`@arSdx~Mmj_As+V}I9BEO^q*+%K-wNLkHZJyKU zKLi5jip5V@Z0o zYt?aX>(_AR`KF!Kn3AY%p;VB#9{_#LLo^F4i@}Ta67!*`6 z7}F+MFu>X+E@$tB1W9n=#t1TE7~4V+i9z{-5t?Md4z)`hv*~LI^57m=0`s5|ZZwTy zV;RKFKeqo;#r9VvEPzF@3YJ10EC=}4Pk0YNeW2OY2rPi*umYA`c0w);0UyBMnBtSc zfkJyLxvLed|g+ zTwd<(=>NL`K?)QZWOtNEWpag5rPgS5$}qvww&QwP^n)ckJnfG2j(zAS#5TQ)8+PfeYHRF8vp = ({ 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) {