From d3d9d440a6b04cb2c47ab624484d130d4f6ae830 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 21 Jan 2022 17:29:08 +0100 Subject: [PATCH] Introduce Reactions and Animated Emoji Interactions (#1583) --- src/@types/global.d.ts | 5 + src/api/gramjs/apiBuilders/appConfig.ts | 46 +++ src/api/gramjs/apiBuilders/chats.ts | 7 + src/api/gramjs/apiBuilders/messages.ts | 57 ++- src/api/gramjs/apiBuilders/misc.ts | 17 +- src/api/gramjs/apiBuilders/symbols.ts | 10 +- src/api/gramjs/methods/chats.ts | 15 + src/api/gramjs/methods/index.ts | 11 +- src/api/gramjs/methods/media.ts | 15 +- src/api/gramjs/methods/messages.ts | 28 +- src/api/gramjs/methods/reactions.ts | 136 +++++++ src/api/gramjs/methods/settings.ts | 26 +- src/api/gramjs/methods/symbols.ts | 15 + src/api/gramjs/updater.ts | 29 +- src/api/types/chats.ts | 2 + src/api/types/messages.ts | 33 ++ src/api/types/misc.ts | 17 + src/api/types/updates.ts | 22 +- src/assets/fonts/icomoon.woff | Bin 40624 -> 41876 bytes src/assets/fonts/icomoon.woff2 | Bin 18992 -> 19684 bytes src/assets/tgs/animatedEmojis/Cumshot.tgs | Bin 0 -> 64906 bytes src/assets/tgs/animatedEmojis/Eggplant.tgs | Bin 0 -> 17805 bytes src/assets/tgs/animatedEmojis/Peach.tgs | Bin 0 -> 30301 bytes .../calls}/CallSchedule.tgs | Bin .../calls}/CameraFlip.tgs | Bin .../calls}/HandFilled.tgs | Bin .../calls}/HandOutline.tgs | Bin .../{animatedIcons => tgs/calls}/Speaker.tgs | Bin .../calls}/VoiceAllowTalk.tgs | Bin .../calls}/VoiceMini.tgs | Bin .../calls}/VoiceMuted.tgs | Bin .../calls}/VoiceOutlined.tgs | Bin .../calls}/VoipGroupRemoved.tgs | Bin .../calls}/VoipInvite.tgs | Bin .../calls}/VoipMuted.tgs | Bin .../calls}/VoipRecordSave.tgs | Bin .../calls}/VoipRecordStart.tgs | Bin .../calls}/VoipUnmuted.tgs | Bin .../monkeys}/TwoFactorSetupMonkeyClose.tgs | Bin .../TwoFactorSetupMonkeyCloseAndPeek.tgs | Bin ...TwoFactorSetupMonkeyCloseAndPeekToIdle.tgs | Bin .../monkeys}/TwoFactorSetupMonkeyIdle.tgs | Bin .../monkeys}/TwoFactorSetupMonkeyPeek.tgs | Bin .../monkeys}/TwoFactorSetupMonkeyTracking.tgs | Bin .../settings}/DiscussionGroupsDucks.tgs | Bin src/assets/{ => tgs/settings}/FoldersAll.tgs | Bin src/assets/{ => tgs/settings}/FoldersNew.tgs | Bin src/bundles/extra.ts | 2 + src/components/common/AnimatedEmoji.tsx | 48 +-- src/components/common/AnimatedSticker.tsx | 5 +- src/components/common/LocalAnimatedEmoji.tsx | 85 +++++ .../common/ReactionStaticEmoji.scss | 4 + src/components/common/ReactionStaticEmoji.tsx | 36 ++ src/components/common/TypingStatus.tsx | 2 +- .../common/helpers/animatedAssets.ts | 39 +- .../common/hooks/useAnimatedEmoji.ts | 155 ++++++++ src/components/left/LeftColumn.tsx | 1 + src/components/left/settings/Settings.scss | 16 + src/components/left/settings/Settings.tsx | 6 + .../left/settings/SettingsGeneral.tsx | 14 + .../left/settings/SettingsHeader.tsx | 2 + .../left/settings/SettingsQuickReaction.tsx | 65 ++++ src/components/main/Main.tsx | 6 +- .../EmojiInteractionAnimation.async.tsx | 15 + .../middle/EmojiInteractionAnimation.scss | 89 +++++ .../middle/EmojiInteractionAnimation.tsx | 122 +++++++ src/components/middle/MessageList.tsx | 18 +- src/components/middle/MessageListContent.tsx | 3 + src/components/middle/MiddleColumn.tsx | 20 +- .../middle/ReactorListModal.async.tsx | 15 + src/components/middle/ReactorListModal.scss | 38 ++ src/components/middle/ReactorListModal.tsx | 203 +++++++++++ .../calculateMiddleFooterTransforms.ts | 4 +- .../middle/message/ContextMenuContainer.tsx | 84 ++++- src/components/middle/message/Message.scss | 46 +++ src/components/middle/message/Message.tsx | 195 +++++++--- .../middle/message/MessageContextMenu.scss | 15 +- .../middle/message/MessageContextMenu.tsx | 157 +++++--- .../middle/message/MessageMeta.scss | 6 + src/components/middle/message/MessageMeta.tsx | 31 +- .../middle/message/ReactionAnimatedEmoji.scss | 50 +++ .../middle/message/ReactionAnimatedEmoji.tsx | 92 +++++ .../middle/message/ReactionButton.tsx | 79 ++++ .../middle/message/ReactionSelector.scss | 91 +++++ .../middle/message/ReactionSelector.tsx | 122 +++++++ src/components/middle/message/Reactions.scss | 84 +++++ src/components/middle/message/Reactions.tsx | 43 +++ .../middle/message/_message-content.scss | 34 ++ .../middle/message/hooks/useOuterHandlers.ts | 124 ++++++- src/components/right/RightColumn.tsx | 1 + src/components/right/RightHeader.tsx | 10 +- .../right/management/ManageChannel.tsx | 20 + .../right/management/ManageGroup.tsx | 21 ++ .../right/management/ManageReactions.tsx | 131 +++++++ .../right/management/Management.scss | 10 + .../right/management/Management.tsx | 10 + src/config.ts | 1 + src/global/cache.ts | 10 + src/global/initial.ts | 1 + src/global/types.ts | 44 ++- src/hooks/useContextMenuPosition.ts | 29 +- src/hooks/useHorizontalScroll.ts | 2 +- src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 28 +- src/lib/gramjs/tl/apiTl.js | 11 +- src/lib/gramjs/tl/static/api.json | 9 +- src/lib/gramjs/tl/static/api.tl | 8 +- src/lib/rlottie/RLottie.ts | 3 + src/modules/actions/all.ts | 1 + src/modules/actions/api/chats.ts | 16 + src/modules/actions/api/messages.ts | 6 +- src/modules/actions/api/reactions.ts | 274 ++++++++++++++ src/modules/actions/api/settings.ts | 13 + src/modules/actions/api/symbols.ts | 15 + src/modules/actions/apiUpdaters/messages.ts | 36 +- src/modules/actions/ui/messages.ts | 16 + src/modules/helpers/messages.ts | 6 +- src/modules/reducers/reactions.ts | 82 +++++ src/modules/selectors/messages.ts | 24 ++ src/modules/selectors/symbols.ts | 37 +- src/styles/Telegram T.json | 344 ++++++++++-------- src/styles/_variables.scss | 10 + src/styles/icons.scss | 6 + src/styles/themes.json | 6 +- src/types/index.ts | 2 + src/util/scrollLock.ts | 10 +- 126 files changed, 3654 insertions(+), 358 deletions(-) create mode 100644 src/api/gramjs/apiBuilders/appConfig.ts create mode 100644 src/api/gramjs/methods/reactions.ts create mode 100644 src/assets/tgs/animatedEmojis/Cumshot.tgs create mode 100644 src/assets/tgs/animatedEmojis/Eggplant.tgs create mode 100644 src/assets/tgs/animatedEmojis/Peach.tgs rename src/assets/{animatedIcons => tgs/calls}/CallSchedule.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/CameraFlip.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/HandFilled.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/HandOutline.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/Speaker.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoiceAllowTalk.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoiceMini.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoiceMuted.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoiceOutlined.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoipGroupRemoved.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoipInvite.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoipMuted.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoipRecordSave.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoipRecordStart.tgs (100%) rename src/assets/{animatedIcons => tgs/calls}/VoipUnmuted.tgs (100%) rename src/assets/{ => tgs/monkeys}/TwoFactorSetupMonkeyClose.tgs (100%) rename src/assets/{ => tgs/monkeys}/TwoFactorSetupMonkeyCloseAndPeek.tgs (100%) rename src/assets/{ => tgs/monkeys}/TwoFactorSetupMonkeyCloseAndPeekToIdle.tgs (100%) rename src/assets/{ => tgs/monkeys}/TwoFactorSetupMonkeyIdle.tgs (100%) rename src/assets/{ => tgs/monkeys}/TwoFactorSetupMonkeyPeek.tgs (100%) rename src/assets/{ => tgs/monkeys}/TwoFactorSetupMonkeyTracking.tgs (100%) rename src/assets/{ => tgs/settings}/DiscussionGroupsDucks.tgs (100%) rename src/assets/{ => tgs/settings}/FoldersAll.tgs (100%) rename src/assets/{ => tgs/settings}/FoldersNew.tgs (100%) create mode 100644 src/components/common/LocalAnimatedEmoji.tsx create mode 100644 src/components/common/ReactionStaticEmoji.scss create mode 100644 src/components/common/ReactionStaticEmoji.tsx create mode 100644 src/components/common/hooks/useAnimatedEmoji.ts create mode 100644 src/components/left/settings/SettingsQuickReaction.tsx create mode 100644 src/components/middle/EmojiInteractionAnimation.async.tsx create mode 100644 src/components/middle/EmojiInteractionAnimation.scss create mode 100644 src/components/middle/EmojiInteractionAnimation.tsx create mode 100644 src/components/middle/ReactorListModal.async.tsx create mode 100644 src/components/middle/ReactorListModal.scss create mode 100644 src/components/middle/ReactorListModal.tsx create mode 100644 src/components/middle/message/ReactionAnimatedEmoji.scss create mode 100644 src/components/middle/message/ReactionAnimatedEmoji.tsx create mode 100644 src/components/middle/message/ReactionButton.tsx create mode 100644 src/components/middle/message/ReactionSelector.scss create mode 100644 src/components/middle/message/ReactionSelector.tsx create mode 100644 src/components/middle/message/Reactions.scss create mode 100644 src/components/middle/message/Reactions.tsx create mode 100644 src/components/right/management/ManageReactions.tsx create mode 100644 src/modules/actions/api/reactions.ts create mode 100644 src/modules/reducers/reactions.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index da96dcf95..10c506dc7 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -18,6 +18,11 @@ declare namespace React { interface VideoHTMLAttributes { srcObject?: MediaStream; } + + interface MouseEvent { + offsetX: number; + offsetY: number; + } } type AnyLiteral = Record; diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts new file mode 100644 index 000000000..c94973e0b --- /dev/null +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import BigInt from 'big-integer'; +import localDb from '../localDb'; +import { Api as GramJs } from '../../../lib/gramjs'; +import { ApiAppConfig } from '../../types'; +import { buildJson } from './misc'; + +type GramJsAppConfig = { + emojies_sounds: Record; + emojies_send_dice: string[]; + groupcall_video_participants_max: number; + reactions_default: string; + reactions_uniq_max: number; +}; + +function buildEmojiSounds(appConfig: GramJsAppConfig) { + const { emojies_sounds } = appConfig; + return Object.keys(emojies_sounds).reduce((acc: Record, key) => { + const l = emojies_sounds[key]; + localDb.documents[l.id] = new GramJs.Document({ + id: BigInt(l.id), + accessHash: BigInt(l.access_hash), + dcId: 1, + mimeType: 'audio/ogg', + fileReference: Buffer.from(atob(l.file_reference_base64 + .replace(/-/g, '+') + .replace(/_/g, '/'))), + } as GramJs.Document); + + acc[key] = l.id; + return acc; + }, {}); +} + +export function buildApiConfig(json: GramJs.TypeJSONValue): ApiAppConfig { + const appConfig = buildJson(json) as GramJsAppConfig; + + return { + emojiSounds: buildEmojiSounds(appConfig), + defaultReaction: appConfig.reactions_default, + }; +} diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 5bdf23bc0..088d7862d 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -303,6 +303,7 @@ export function buildChatTypingStatus( serverTimeOffset: number, ) { let action: string = ''; + let emoticon: string | undefined; if (update.action instanceof GramJs.SendMessageCancelAction) { return undefined; } else if (update.action instanceof GramJs.SendMessageTypingAction) { @@ -333,10 +334,16 @@ export function buildChatTypingStatus( action = 'lng_send_action_choose_sticker'; } else if (update.action instanceof GramJs.SpeakingInGroupCallAction) { return undefined; + } else if (update.action instanceof GramJs.SendMessageEmojiInteractionSeen) { + action = 'lng_user_action_watching_animations'; + emoticon = update.action.emoticon; + } else if (update.action instanceof GramJs.SendMessageEmojiInteraction) { + return undefined; } return { action, + ...(emoticon && { emoji: emoticon }), ...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }), timestamp: Date.now() + serverTimeOffset * 1000, }; diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index de934b855..a6920477d 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -22,8 +22,12 @@ import { ApiThreadInfo, ApiInvoice, ApiGroupCall, - ApiUser, + ApiReactions, + ApiReactionCount, + ApiUserReaction, + ApiAvailableReaction, ApiSponsoredMessage, + ApiUser, } from '../../types'; import { @@ -141,7 +145,7 @@ type UniversalMessage = ( & Pick, ( 'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' | 'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' | - 'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' + 'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' )> ); @@ -192,6 +196,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId, views: mtpMessage.views, isFromScheduled: mtpMessage.fromScheduled, + reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions), ...(replyToMsgId && { replyToMessageId: replyToMsgId }), ...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }), ...(replyToTopId && { replyToTopMessageId: replyToTopId }), @@ -214,6 +219,54 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM }; } +export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions { + const { + recentReactons, results, canSeeList, + } = reactions; + + return { + canSeeList, + results: results.map(buildReactionCount), + recentReactions: recentReactons?.map(buildMessageUserReaction), + }; +} + +function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount { + const { chosen, count, reaction } = reactionCount; + + return { + isChosen: chosen, + count, + reaction, + }; +} + +export function buildMessageUserReaction(userReaction: GramJs.MessageUserReaction): ApiUserReaction { + const { userId, reaction } = userReaction; + + return { + userId: buildApiPeerId(userId, 'user'), + reaction, + }; +} + +export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction { + const { + selectAnimation, staticIcon, reaction, title, + inactive, aroundAnimation, centerIcon, + } = availableReaction; + + return { + selectAnimation: buildApiDocument(selectAnimation), + staticIcon: buildApiDocument(staticIcon), + aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined, + centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined, + reaction, + title, + isInactive: inactive, + }; +} + export function buildMessageTextContent( message: string, entities?: GramJs.TypeMessageEntity[], diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 9c1ceb32b..0e327c405 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -1,6 +1,8 @@ import { Api as GramJs } from '../../../lib/gramjs'; -import { ApiCountry, ApiSession, ApiWallpaper } from '../../types'; +import { + ApiCountry, ApiSession, ApiWallpaper, +} from '../../types'; import { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types'; import { buildApiDocument } from './messages'; @@ -155,3 +157,16 @@ export function buildApiCountryList(countries: GramJs.help.Country[]) { general: generalList, }; } + +export function buildJson(json: GramJs.TypeJSONValue): any { + if (json instanceof GramJs.JsonNull) return undefined; + if (json instanceof GramJs.JsonString + || json instanceof GramJs.JsonBool + || json instanceof GramJs.JsonNumber) return json.value; + if (json instanceof GramJs.JsonArray) return json.value.map(buildJson); + + return json.value.reduce((acc: Record, el) => { + acc[el.key] = buildJson(el.value); + return acc; + }, {}); +} diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index 793f95146..ed07574ff 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -1,5 +1,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; -import { ApiSticker, ApiStickerSet } from '../../types'; +import { + ApiEmojiInteraction, ApiSticker, ApiStickerSet, GramJsEmojiInteraction, +} from '../../types'; import { MEMOJI_STICKER_ID } from '../../../config'; import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common'; @@ -108,3 +110,9 @@ export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetC return stickerSet; } + +export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmojiInteraction { + return { + timestamps: json.a.map((l) => l.t), + }; +} diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index d0edd3948..c7bc0db48 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -341,6 +341,7 @@ async function getFullChatInfo(chatId: string): Promise<{ exportedInvite, botInfo, call, + availableReactions, } = result.fullChat; const members = buildChatMembers(participants); @@ -358,6 +359,7 @@ async function getFullChatInfo(chatId: string): Promise<{ inviteLink: exportedInvite.link, }), groupCallId: call?.id.toString(), + enabledReactions: availableReactions, }, users: result.users.map(buildApiUser).filter(Boolean as any), groupCall: call ? { @@ -403,6 +405,7 @@ async function getFullChannelInfo( hiddenPrehistory, call, botInfo, + availableReactions, defaultSendAs, } = result.fullChat; @@ -454,6 +457,7 @@ async function getFullChannelInfo( groupCallId: call ? String(call.id) : undefined, linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined, botCommands, + enabledReactions: availableReactions, sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined, }, users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], @@ -1143,6 +1147,17 @@ export async function importChatInvite({ hash }: { hash: string }) { return buildApiChatFromPreview(updates.chats[0]); } +export function setChatEnabledReactions({ + chat, enabledReactions, +}: { + chat: ApiChat; enabledReactions: string[]; +}) { + return invokeRequest(new GramJs.messages.SetChatAvailableReactions({ + peer: buildInputPeer(chat.id, chat.accessHash), + availableReactions: enabledReactions, + }), true); +} + export function toggleIsProtected({ chat, isProtected, }: { chat: ApiChat; isProtected: boolean }) { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 42df839a2..b3659fb60 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -10,7 +10,7 @@ export { fetchChats, fetchFullChat, searchChats, requestChatUpdate, saveDraft, clearDraft, fetchChat, updateChatMutedState, createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto, - toggleChatPinned, toggleChatArchived, toggleDialogUnread, + toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions, fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders, getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights, updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup, @@ -34,7 +34,7 @@ export { export { fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers, faveSticker, fetchStickers, fetchSavedGifs, searchStickers, installStickerSet, uninstallStickerSet, - searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, + searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects, } from './symbols'; export { @@ -47,7 +47,7 @@ export { fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings, fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice, - updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, + updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig, } from './settings'; export { @@ -67,3 +67,8 @@ export { editGroupCallTitle, editGroupCallParticipant, exportGroupCallInvite, fetchGroupCallParticipants, joinGroupCallPresentation, leaveGroupCall, leaveGroupCallPresentation, toggleGroupCallStartSubscription, } from './calls'; + +export { + getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList, + setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction, +} from './reactions'; diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index fbfa6ee9c..92e7ccde1 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -17,9 +17,10 @@ import { getEntityTypeById } from '../gramjsBuilders'; import * as cacheApi from '../../../util/cacheApi'; type EntityType = ( - 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' + 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' | + 'document' ); -const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument']); +const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']); export default async function downloadMedia( { @@ -75,7 +76,9 @@ async function download( ) { const mediaMatch = url.startsWith('webDocument') ? url.match(/(webDocument):(.+)/) - : url.match(/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/); + : url.match( + /(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/, + ); if (!mediaMatch) { return undefined; } @@ -102,7 +105,8 @@ async function download( if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') { entityType = getEntityTypeById(entityId); } else { - entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument'; + entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument' | + 'document'; } switch (entityType) { @@ -130,6 +134,9 @@ async function download( case 'webDocument': entity = localDb.webDocuments[entityId]; break; + case 'document': + entity = localDb.documents[entityId]; + break; } if (!entity) { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 36b400525..93af244f8 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1,21 +1,21 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { - ApiChat, ApiAttachment, + ApiChat, + ApiGlobalMessageSearchType, ApiMessage, - OnApiUpdate, - ApiMessageSearchType, - ApiUser, - ApiSticker, - ApiVideo, - ApiNewPoll, ApiMessageEntity, + ApiMessageSearchType, + ApiNewPoll, ApiOnProgress, + ApiReportReason, + ApiSticker, ApiThreadInfo, + ApiUser, + ApiVideo, MAIN_THREAD_ID, MESSAGE_DELETED, - ApiGlobalMessageSearchType, - ApiReportReason, + OnApiUpdate, ApiSponsoredMessage, ApiSendMessageAction, ApiContact, @@ -31,23 +31,23 @@ import { import { invokeRequest, uploadFile } from './client'; import { buildApiMessage, + buildLocalForwardedMessage, buildLocalMessage, buildWebPage, - buildLocalForwardedMessage, buildApiSponsoredMessage, } from '../apiBuilders/messages'; import { buildApiUser } from '../apiBuilders/users'; import { buildInputEntity, + buildInputMediaDocument, buildInputPeer, + buildInputPoll, + buildInputReportReason, + buildMtpMessageEntity, generateRandomBigInt, getEntityTypeById, - buildInputMediaDocument, - buildInputPoll, - buildMtpMessageEntity, isMessageWithMedia, isServiceMessageWithMedia, - buildInputReportReason, buildSendMessageAction, } from '../gramjsBuilders'; import localDb from '../localDb'; diff --git a/src/api/gramjs/methods/reactions.ts b/src/api/gramjs/methods/reactions.ts new file mode 100644 index 000000000..1a086a1ba --- /dev/null +++ b/src/api/gramjs/methods/reactions.ts @@ -0,0 +1,136 @@ +import { ApiChat, ApiUser } from '../../types'; +import { invokeRequest } from './client'; +import { Api as GramJs } from '../../../lib/gramjs'; +import { buildInputPeer } from '../gramjsBuilders'; +import localDb from '../localDb'; +import { buildApiAvailableReaction, buildMessageUserReaction } from '../apiBuilders/messages'; +import { REACTION_LIST_LIMIT } from '../../../config'; +import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import { buildApiUser } from '../apiBuilders/users'; + +export function sendWatchingEmojiInteraction({ + chat, + emoticon, +}: { + chat: ApiChat; emoticon: string; +}) { + return invokeRequest(new GramJs.messages.SetTyping({ + peer: buildInputPeer(chat.id, chat.accessHash), + action: new GramJs.SendMessageEmojiInteractionSeen({ + emoticon, + }), + })); +} + +export function sendEmojiInteraction({ + chat, + emoticon, + messageId, + timestamps, +}: { + chat: ApiChat; messageId: number; emoticon: string; timestamps: number[]; +}) { + return invokeRequest(new GramJs.messages.SetTyping({ + peer: buildInputPeer(chat.id, chat.accessHash), + action: new GramJs.SendMessageEmojiInteraction({ + emoticon, + msgId: messageId, + interaction: new GramJs.DataJSON({ + data: JSON.stringify({ + v: 1, + a: timestamps.map((t: number) => ({ + t, + i: 1, + })), + }), + }), + }), + })); +} + +export async function getAvailableReactions() { + const result = await invokeRequest(new GramJs.messages.GetAvailableReactions({})); + + if (!result || result instanceof GramJs.messages.AvailableReactionsNotModified) { + return undefined; + } + + result.reactions.forEach((reaction) => { + if (reaction.staticIcon instanceof GramJs.Document) { + localDb.documents[String(reaction.staticIcon.id)] = reaction.staticIcon; + } + if (reaction.selectAnimation instanceof GramJs.Document) { + localDb.documents[String(reaction.selectAnimation.id)] = reaction.selectAnimation; + } + if (reaction.aroundAnimation instanceof GramJs.Document) { + localDb.documents[String(reaction.aroundAnimation.id)] = reaction.aroundAnimation; + } + if (reaction.centerIcon instanceof GramJs.Document) { + localDb.documents[String(reaction.centerIcon.id)] = reaction.centerIcon; + } + }); + + return result.reactions.map(buildApiAvailableReaction); +} + +export function sendReaction({ + chat, messageId, reaction, +}: { + chat: ApiChat; messageId: number; reaction?: string; +}) { + return invokeRequest(new GramJs.messages.SendReaction({ + ...(reaction && { reaction }), + peer: buildInputPeer(chat.id, chat.accessHash), + msgId: messageId, + }), true); +} + +export function fetchMessageReactions({ + ids, chat, +}: { + ids: number[]; chat: ApiChat; +}) { + return invokeRequest(new GramJs.messages.GetMessagesReactions({ + id: ids, + peer: buildInputPeer(chat.id, chat.accessHash), + }), true); +} + +export async function fetchMessageReactionsList({ + chat, messageId, reaction, offset, +}: { + chat: ApiChat; messageId: number; reaction?: string; offset?: string; +}) { + const result = await invokeRequest(new GramJs.messages.GetMessageReactionsList({ + peer: buildInputPeer(chat.id, chat.accessHash), + id: messageId, + ...(reaction && { reaction }), + limit: REACTION_LIST_LIMIT, + ...(offset && { offset }), + })); + + if (!result) { + return undefined; + } + + addEntitiesWithPhotosToLocalDb(result.users); + + const { nextOffset, reactions, count } = result; + + return { + users: result.users.map(buildApiUser).filter(Boolean as any), + nextOffset, + reactions: reactions.map(buildMessageUserReaction), + count, + }; +} + +export function setDefaultReaction({ + reaction, +}: { + reaction: string; +}) { + return invokeRequest(new GramJs.messages.SetDefaultReaction({ + reaction, + })); +} diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index d69b12b03..d25b1497b 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -2,24 +2,35 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import { - ApiChat, ApiLangString, ApiLanguage, ApiNotifyException, ApiUser, ApiWallpaper, + ApiAppConfig, + ApiChat, + ApiLangString, + ApiLanguage, + ApiNotifyException, + ApiUser, + ApiWallpaper, } from '../../types'; import { ApiPrivacyKey, InputPrivacyRules, LangCode } from '../../../types'; import { BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK, LANG_PACKS } from '../../../config'; import { - buildApiWallpaper, buildApiSession, buildPrivacyRules, buildApiNotifyException, buildApiCountryList, + buildApiCountryList, + buildApiNotifyException, + buildApiSession, + buildApiWallpaper, + buildPrivacyRules, } from '../apiBuilders/misc'; import { buildApiUser } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; -import { buildInputPrivacyKey, buildInputPeer, buildInputEntity } from '../gramjsBuilders'; -import { invokeRequest, uploadFile, getClient } from './client'; +import { buildInputEntity, buildInputPeer, buildInputPrivacyKey } from '../gramjsBuilders'; +import { getClient, invokeRequest, uploadFile } from './client'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildCollectionByKey } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import localDb from '../localDb'; +import { buildApiConfig } from '../apiBuilders/appConfig'; const MAX_INT_32 = 2 ** 31 - 1; const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz']; @@ -429,6 +440,13 @@ export function updateContentSettings(isEnabled: boolean) { })); } +export async function fetchAppConfig(): Promise { + const result = await invokeRequest(new GramJs.help.GetAppConfig()); + if (!result) return undefined; + + return buildApiConfig(result); +} + function updateLocalDb( result: ( GramJs.account.PrivacyRules | GramJs.contacts.Blocked | GramJs.contacts.BlockedSlice | diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index f520605fd..0a21fe6c8 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -130,6 +130,21 @@ export async function fetchAnimatedEmojis() { }; } +export async function fetchAnimatedEmojiEffects() { + const result = await invokeRequest(new GramJs.messages.GetStickerSet({ + stickerset: new GramJs.InputStickerSetAnimatedEmojiAnimations(), + })); + + if (!(result instanceof GramJs.messages.StickerSet)) { + return undefined; + } + + return { + set: buildStickerSet(result.set), + stickers: processStickerResult(result.documents), + }; +} + export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) { const result = await invokeRequest(new GramJs.messages.SearchStickerSets({ q: query, diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 7220a1677..f20a35e83 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -13,6 +13,7 @@ import { buildPollResults, buildApiMessageFromNotification, buildMessageDraft, + buildMessageReactions, } from './apiBuilders/messages'; import { buildChatMember, @@ -46,6 +47,7 @@ import { getGroupCallId, } from './apiBuilders/calls'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; +import { buildApiEmojiInteraction } from './apiBuilders/symbols'; type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } @@ -292,6 +294,13 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { chatId: message.chatId, message, }); + } else if (update instanceof GramJs.UpdateMessageReactions) { + onUpdate({ + '@type': 'updateMessageReactions', + id: update.msgId, + chatId: getApiChatIdFromMtpPeer(update.peer), + reactions: buildMessageReactions(update.reactions), + }); } else if (update instanceof GramJs.UpdateDeleteMessages) { onUpdate({ '@type': 'deleteMessages', @@ -614,11 +623,21 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { ? buildApiPeerId(update.userId, 'user') : buildApiPeerId(update.chatId, 'chat'); - onUpdate({ - '@type': 'updateChatTypingStatus', - id, - typingStatus: buildChatTypingStatus(update, serverTimeOffset), - }); + if (update.action instanceof GramJs.SendMessageEmojiInteraction) { + onUpdate({ + '@type': 'updateStartEmojiInteraction', + id, + emoji: update.action.emoticon, + messageId: update.action.msgId, + interaction: buildApiEmojiInteraction(JSON.parse(update.action.interaction.data)), + }); + } else { + onUpdate({ + '@type': 'updateChatTypingStatus', + id, + typingStatus: buildChatTypingStatus(update, serverTimeOffset), + }); + } } else if (update instanceof GramJs.UpdateChannelUserTyping) { const id = buildApiPeerId(update.channelId, 'channel'); diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 69be7ebdb..436620fc3 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -64,6 +64,7 @@ export interface ApiTypingStatus { userId?: string; action: string; timestamp: number; + emoji?: string; } export interface ApiChatFullInfo { @@ -86,6 +87,7 @@ export interface ApiChatFullInfo { }; linkedChatId?: string; botCommands?: ApiBotCommand[]; + enabledReactions?: string[]; sendAsId?: string; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index ca6ddccf7..8acaa8cf8 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -268,6 +268,39 @@ export interface ApiMessage { isFromScheduled?: boolean; seenByUserIds?: string[]; isProtected?: boolean; + reactors?: { + nextOffset?: string; + count: number; + reactions: ApiUserReaction[]; + }; + reactions?: ApiReactions; +} + +export interface ApiReactions { + canSeeList?: boolean; + results: ApiReactionCount[]; + recentReactions?: ApiUserReaction[]; +} + +export interface ApiUserReaction { + userId: string; + reaction: string; +} + +export interface ApiReactionCount { + isChosen?: boolean; + count: number; + reaction: string; +} + +export interface ApiAvailableReaction { + selectAnimation?: ApiDocument; + staticIcon?: ApiDocument; + centerIcon?: ApiDocument; + aroundAnimation?: ApiDocument; + reaction: string; + title: string; + isInactive?: boolean; } export interface ApiThreadInfo { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 40ca9ef5c..7c83a1457 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -109,3 +109,20 @@ export interface ApiCountryCode extends ApiCountry { prefixes?: string[]; patterns?: string[]; } + +export interface ApiAppConfig { + emojiSounds: Record; + defaultReaction: string; +} + +export interface GramJsEmojiInteraction { + v: number; + a: { + i: number; + t: number; + }[]; +} + +export interface ApiEmojiInteraction { + timestamps: number[]; +} diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 4bc3b0d04..9ae2a169a 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -7,10 +7,11 @@ import { ApiChatFolder, } from './chats'; import { - ApiFormattedText, ApiMessage, ApiPhoto, ApiPoll, ApiStickerSet, ApiThreadInfo, + ApiFormattedText, ApiMessage, ApiPhoto, ApiPoll, ApiReactions, ApiStickerSet, ApiThreadInfo, } from './messages'; import { ApiUser, ApiUserFullInfo, ApiUserStatus } from './users'; import { + ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData, } from './misc'; import { @@ -102,6 +103,14 @@ export type ApiUpdateChatTypingStatus = { typingStatus: ApiTypingStatus | undefined; }; +export type ApiUpdateStartEmojiInteraction = { + '@type': 'updateStartEmojiInteraction'; + id: string; + emoji: string; + messageId: number; + interaction: ApiEmojiInteraction; +}; + export type ApiUpdateChatFullInfo = { '@type': 'updateChatFullInfo'; id: string; @@ -284,6 +293,13 @@ export type ApiUpdateDraftMessage = { replyingToId?: number; }; +export type ApiUpdateMessageReactions = { + '@type': 'updateMessageReactions'; + id: number; + chatId: string; + reactions: ApiReactions; +}; + export type ApiDeleteContact = { '@type': 'deleteContact'; id: string; @@ -435,13 +451,13 @@ export type ApiUpdate = ( ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos | ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | - ApiUpdateError | ApiUpdateResetContacts | + ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction | ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | - ApiUpdateServerTimeOffset | ApiUpdateShowInvite | + ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams | ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId ); diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 9cfcfcb6f764b8613b4792db71f7ef36c9b36e3a..2f403f5efdc30f3eac3418f0fffa3c710074285f 100644 GIT binary patch delta 1564 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 SBN7sd z;wf>Rb{~Ay2c5G#^?|-Kb8$0mg**C|q*g9YXK5Z^JR7)AwRt_bn-d^;2rbIDN}+uD z^1&}0_MJaPX5sFm0OmZqZ~uEML?943`~UhLv~U>P$FLYoeBa7uTVD(RzmwtT^PTXV zH-J%?NQv@QeYNo;ZrqtApK_X0{Ua)iKc0HpPK#6(^

zQDgk}L+>;?`q0ert^>aIWK5bTT^Kg`Bave&Ak=>d0N}ri^sBb)wfNfW*p~ji7IufY z8@8H}cI8)J)%(=jeeL5S)l$T4Bd$_)_#W=`fjfy#-%d$40#p|VsQ=2S+Ms%(&R^W+ zVX9*EMf;SUI@OyTk5JED81TjqD3L}2{(!hpsTxBdZ%UqVQZlo`tD*1>)#RGPaIDh0 z=)oV@?f|>$Ew)Fb56A2sSl;$EHr{J0#T$?YbmE0DpgAbygLY+Oy*_PPJUt_s*|<^l z`Iq9+CM6SJeyKWoRGaCX(?>_tEv?g_9bsdG2OWC;f_~bLD5|`Zo|0)q@cT6n^StIJ zBJk0gkwM8pevjqk3=t%xe}lK`LkuOp?+M{#E4(kfS0%|LH$Vwo(>1JzuTG?6a7Mh4 zXR*bVa7`Gn`_0A02FCeKa3k|WxZp@IO#uT$H%;Cq6Kp`hZDt@Nk>>J}<#T^zzWlwA zGAEdKu}HmH-s}x0$AhxTWn4I3*+?qcNNz>(aV>d9;?vcd8!%(VdwT}DKVdCoo5A_g zvxjGTE|9JdKsE#kTyX)wC={!FMB>^O#gEBYiG?y`@67*K!{;$P^m_S-Jw8LioWdzV zSPIjmL?sP^W<{Ve+m2K`7ccaot*2hoX-eRq!lF_lV}szZ23lGDohrs|jcT-5LZf=$ zY(4&C`D0jq6{=IO!(ui|fxq9xJHV!AW(``@S*l&*sX(*^qJTdW#fOFIV!k##9m_6s z_3GSsFJTiy8?{we?G4>#=93!>P7M2p_0P+3m?p#jsol8vFL?L|>;#||LTca)M$Kf_ zEmoV|;dEhi2F_#=Yz~)4Qhb3>B$h~Ja)nZ*)@XI67A#t_Y{jZI>o#oKvTet%J^KzE zg1`_c430pe&=@QZPau-W6e^9*V6xa8E{`t|io_DBOs-I>)Ecc$Z!nt77OTzfaJt+c zuaB>vziz9%W3@GU^y;(LI_qt)(I)TOY>R$dZ8KoI9R^$d$oBW^sqoqQ=lpSu_rogA zA}1iRsJYx8udiS6FVDjXNGxhDx5w-2SNu0PHUIzs00000AR;0nA|fIp5)ly*5fKp) rF*7qWGcz+Yi-?Gbh=_=YsH&=}s;a80nwgoInVFfHxsq!d0j>l9%&4F^ literal 18992 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 literal 0 HcmV?d00001 diff --git a/src/assets/tgs/animatedEmojis/Peach.tgs b/src/assets/tgs/animatedEmojis/Peach.tgs new file mode 100644 index 0000000000000000000000000000000000000000..78d40f9f32cfff19a52841ed003eebce4af7b3e9 GIT binary patch literal 30301 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 Aa{vGU literal 0 HcmV?d00001 diff --git a/src/assets/animatedIcons/CallSchedule.tgs b/src/assets/tgs/calls/CallSchedule.tgs similarity index 100% rename from src/assets/animatedIcons/CallSchedule.tgs rename to src/assets/tgs/calls/CallSchedule.tgs diff --git a/src/assets/animatedIcons/CameraFlip.tgs b/src/assets/tgs/calls/CameraFlip.tgs similarity index 100% rename from src/assets/animatedIcons/CameraFlip.tgs rename to src/assets/tgs/calls/CameraFlip.tgs diff --git a/src/assets/animatedIcons/HandFilled.tgs b/src/assets/tgs/calls/HandFilled.tgs similarity index 100% rename from src/assets/animatedIcons/HandFilled.tgs rename to src/assets/tgs/calls/HandFilled.tgs diff --git a/src/assets/animatedIcons/HandOutline.tgs b/src/assets/tgs/calls/HandOutline.tgs similarity index 100% rename from src/assets/animatedIcons/HandOutline.tgs rename to src/assets/tgs/calls/HandOutline.tgs diff --git a/src/assets/animatedIcons/Speaker.tgs b/src/assets/tgs/calls/Speaker.tgs similarity index 100% rename from src/assets/animatedIcons/Speaker.tgs rename to src/assets/tgs/calls/Speaker.tgs diff --git a/src/assets/animatedIcons/VoiceAllowTalk.tgs b/src/assets/tgs/calls/VoiceAllowTalk.tgs similarity index 100% rename from src/assets/animatedIcons/VoiceAllowTalk.tgs rename to src/assets/tgs/calls/VoiceAllowTalk.tgs diff --git a/src/assets/animatedIcons/VoiceMini.tgs b/src/assets/tgs/calls/VoiceMini.tgs similarity index 100% rename from src/assets/animatedIcons/VoiceMini.tgs rename to src/assets/tgs/calls/VoiceMini.tgs diff --git a/src/assets/animatedIcons/VoiceMuted.tgs b/src/assets/tgs/calls/VoiceMuted.tgs similarity index 100% rename from src/assets/animatedIcons/VoiceMuted.tgs rename to src/assets/tgs/calls/VoiceMuted.tgs diff --git a/src/assets/animatedIcons/VoiceOutlined.tgs b/src/assets/tgs/calls/VoiceOutlined.tgs similarity index 100% rename from src/assets/animatedIcons/VoiceOutlined.tgs rename to src/assets/tgs/calls/VoiceOutlined.tgs diff --git a/src/assets/animatedIcons/VoipGroupRemoved.tgs b/src/assets/tgs/calls/VoipGroupRemoved.tgs similarity index 100% rename from src/assets/animatedIcons/VoipGroupRemoved.tgs rename to src/assets/tgs/calls/VoipGroupRemoved.tgs diff --git a/src/assets/animatedIcons/VoipInvite.tgs b/src/assets/tgs/calls/VoipInvite.tgs similarity index 100% rename from src/assets/animatedIcons/VoipInvite.tgs rename to src/assets/tgs/calls/VoipInvite.tgs diff --git a/src/assets/animatedIcons/VoipMuted.tgs b/src/assets/tgs/calls/VoipMuted.tgs similarity index 100% rename from src/assets/animatedIcons/VoipMuted.tgs rename to src/assets/tgs/calls/VoipMuted.tgs diff --git a/src/assets/animatedIcons/VoipRecordSave.tgs b/src/assets/tgs/calls/VoipRecordSave.tgs similarity index 100% rename from src/assets/animatedIcons/VoipRecordSave.tgs rename to src/assets/tgs/calls/VoipRecordSave.tgs diff --git a/src/assets/animatedIcons/VoipRecordStart.tgs b/src/assets/tgs/calls/VoipRecordStart.tgs similarity index 100% rename from src/assets/animatedIcons/VoipRecordStart.tgs rename to src/assets/tgs/calls/VoipRecordStart.tgs diff --git a/src/assets/animatedIcons/VoipUnmuted.tgs b/src/assets/tgs/calls/VoipUnmuted.tgs similarity index 100% rename from src/assets/animatedIcons/VoipUnmuted.tgs rename to src/assets/tgs/calls/VoipUnmuted.tgs diff --git a/src/assets/TwoFactorSetupMonkeyClose.tgs b/src/assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs similarity index 100% rename from src/assets/TwoFactorSetupMonkeyClose.tgs rename to src/assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs diff --git a/src/assets/TwoFactorSetupMonkeyCloseAndPeek.tgs b/src/assets/tgs/monkeys/TwoFactorSetupMonkeyCloseAndPeek.tgs similarity index 100% rename from src/assets/TwoFactorSetupMonkeyCloseAndPeek.tgs rename to src/assets/tgs/monkeys/TwoFactorSetupMonkeyCloseAndPeek.tgs diff --git a/src/assets/TwoFactorSetupMonkeyCloseAndPeekToIdle.tgs b/src/assets/tgs/monkeys/TwoFactorSetupMonkeyCloseAndPeekToIdle.tgs similarity index 100% rename from src/assets/TwoFactorSetupMonkeyCloseAndPeekToIdle.tgs rename to src/assets/tgs/monkeys/TwoFactorSetupMonkeyCloseAndPeekToIdle.tgs diff --git a/src/assets/TwoFactorSetupMonkeyIdle.tgs b/src/assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs similarity index 100% rename from src/assets/TwoFactorSetupMonkeyIdle.tgs rename to src/assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs diff --git a/src/assets/TwoFactorSetupMonkeyPeek.tgs b/src/assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs similarity index 100% rename from src/assets/TwoFactorSetupMonkeyPeek.tgs rename to src/assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs diff --git a/src/assets/TwoFactorSetupMonkeyTracking.tgs b/src/assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs similarity index 100% rename from src/assets/TwoFactorSetupMonkeyTracking.tgs rename to src/assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs diff --git a/src/assets/DiscussionGroupsDucks.tgs b/src/assets/tgs/settings/DiscussionGroupsDucks.tgs similarity index 100% rename from src/assets/DiscussionGroupsDucks.tgs rename to src/assets/tgs/settings/DiscussionGroupsDucks.tgs diff --git a/src/assets/FoldersAll.tgs b/src/assets/tgs/settings/FoldersAll.tgs similarity index 100% rename from src/assets/FoldersAll.tgs rename to src/assets/tgs/settings/FoldersAll.tgs diff --git a/src/assets/FoldersNew.tgs b/src/assets/tgs/settings/FoldersNew.tgs similarity index 100% rename from src/assets/FoldersNew.tgs rename to src/assets/tgs/settings/FoldersNew.tgs diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 370a80fcb..1f8d58839 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -12,6 +12,8 @@ export { default as PinMessageModal } from '../components/common/PinMessageModal export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllMessagesModal'; export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar'; export { default as SeenByModal } from '../components/common/SeenByModal'; +export { default as ReactorListModal } from '../components/middle/ReactorListModal'; +export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation'; export { default as LeftSearch } from '../components/left/search/LeftSearch'; export { default as Settings } from '../components/left/settings/Settings'; diff --git a/src/components/common/AnimatedEmoji.tsx b/src/components/common/AnimatedEmoji.tsx index 14e9997dc..a98ef8580 100644 --- a/src/components/common/AnimatedEmoji.tsx +++ b/src/components/common/AnimatedEmoji.tsx @@ -1,14 +1,15 @@ import React, { - FC, useCallback, useRef, useState, + FC, memo, } from '../../lib/teact/teact'; import { ApiMediaFormat, ApiSticker } from '../../api/types'; +import { ActiveEmojiInteraction } from '../../global/types'; import { LIKE_STICKER_ID } from './helpers/mediaDimensions'; import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver'; import useMedia from '../../hooks/useMedia'; import useMediaTransition from '../../hooks/useMediaTransition'; -import useFlag from '../../hooks/useFlag'; +import useAnimatedEmoji from './hooks/useAnimatedEmoji'; import AnimatedSticker from './AnimatedSticker'; @@ -16,30 +17,43 @@ import './AnimatedEmoji.scss'; type OwnProps = { sticker: ApiSticker; + effect?: ApiSticker; + isOwn?: boolean; + soundId?: string; observeIntersection?: ObserveFn; size?: 'large' | 'medium' | 'small'; lastSyncTime?: number; forceLoadPreview?: boolean; + messageId?: number; + chatId?: string; + activeEmojiInteraction?: ActiveEmojiInteraction; }; const QUALITY = 1; -const WIDTH = { - large: 160, - medium: 128, - small: 104, -}; const AnimatedEmoji: FC = ({ sticker, + effect, + isOwn, + soundId, size = 'medium', observeIntersection, lastSyncTime, forceLoadPreview, + messageId, + chatId, + activeEmojiInteraction, }) => { - // eslint-disable-next-line no-null/no-null - const ref = useRef(null); + const { + markAnimationLoaded, + isAnimationLoaded, + ref, + width, + style, + handleClick, + playKey, + } = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteraction, isOwn, undefined, effect?.emoji); - const [isAnimationLoaded, markAnimationLoaded] = useFlag(); const localMediaHash = `sticker${sticker.id}`; const isIntersecting = useIsIntersecting(ref, observeIntersection); @@ -56,19 +70,11 @@ const AnimatedEmoji: FC = ({ const mediaData = useMedia(localMediaHash, !isIntersecting, ApiMediaFormat.Lottie, lastSyncTime); const isMediaLoaded = Boolean(mediaData); - const [playKey, setPlayKey] = useState(String(Math.random())); - const handleClick = useCallback(() => { - setPlayKey(String(Math.random())); - }, []); - - const width = WIDTH[size]; - const style = `width: ${width}px; height: ${width}px;`; - return (

@@ -78,7 +84,7 @@ const AnimatedEmoji: FC = ({ {!isAnimationLoaded && previewBlobUrl && ( )} - {isMediaLoaded && ( + {isMediaLoaded && localMediaHash && ( = ({ ); }; -export default AnimatedEmoji; +export default memo(AnimatedEmoji); diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 49cc6f903..f042c299d 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -20,6 +20,7 @@ type OwnProps = { isLowPriority?: boolean; onLoad?: NoneToVoidFunction; color?: [number, number, number]; + onEnded?: NoneToVoidFunction; }; type RLottieClass = typeof import('../../lib/rlottie/RLottie').default; @@ -54,6 +55,7 @@ const AnimatedSticker: FC = ({ isLowPriority, onLoad, color, + onEnded, }) => { const [animation, setAnimation] = useState(); // eslint-disable-next-line no-null/no-null @@ -89,6 +91,7 @@ const AnimatedSticker: FC = ({ }, onLoad, color, + onEnded, ); if (speed) { @@ -109,7 +112,7 @@ const AnimatedSticker: FC = ({ }); }); } - }, [color, animation, animationData, id, isLowPriority, noLoop, onLoad, quality, size, speed]); + }, [color, animation, animationData, id, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded]); useEffect(() => { if (!animation) return; diff --git a/src/components/common/LocalAnimatedEmoji.tsx b/src/components/common/LocalAnimatedEmoji.tsx new file mode 100644 index 000000000..3ae7747b0 --- /dev/null +++ b/src/components/common/LocalAnimatedEmoji.tsx @@ -0,0 +1,85 @@ +import React, { + FC, memo, useEffect, useState, +} from '../../lib/teact/teact'; + +import { ActiveEmojiInteraction } from '../../global/types'; + +import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver'; +import getAnimationData, { ANIMATED_STICKERS_PATHS } from './helpers/animatedAssets'; +import useAnimatedEmoji from './hooks/useAnimatedEmoji'; + +import AnimatedSticker from './AnimatedSticker'; + +const QUALITY = 1; + +type OwnProps = { + localSticker?: string; + localEffect?: string; + isOwn?: boolean; + soundId?: string; + observeIntersection?: ObserveFn; + size?: 'large' | 'medium' | 'small'; + lastSyncTime?: number; + forceLoadPreview?: boolean; + messageId?: number; + chatId?: string; + activeEmojiInteraction?: ActiveEmojiInteraction; +}; + +const LocalAnimatedEmoji: FC = ({ + localSticker, + localEffect, + isOwn, + soundId, + size = 'medium', + observeIntersection, + messageId, + chatId, + activeEmojiInteraction, +}) => { + const { + playKey, + ref, + style, + width, + handleClick, + markAnimationLoaded, + } = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteraction, isOwn, localEffect); + const id = `local_emoji_${localSticker}`; + + const isIntersecting = useIsIntersecting(ref, observeIntersection); + + const [localStickerAnimationData, setLocalStickerAnimationData] = useState(); + useEffect(() => { + if (localSticker) { + getAnimationData(localSticker as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => { + setLocalStickerAnimationData(data); + }); + } + }, [localSticker]); + + return ( +
+ {localStickerAnimationData && ( + + )} +
+ ); +}; + +export default memo(LocalAnimatedEmoji); diff --git a/src/components/common/ReactionStaticEmoji.scss b/src/components/common/ReactionStaticEmoji.scss new file mode 100644 index 000000000..d06876964 --- /dev/null +++ b/src/components/common/ReactionStaticEmoji.scss @@ -0,0 +1,4 @@ +.ReactionStaticEmoji { + width: 1rem; + display: block; +} diff --git a/src/components/common/ReactionStaticEmoji.tsx b/src/components/common/ReactionStaticEmoji.tsx new file mode 100644 index 000000000..5079598f0 --- /dev/null +++ b/src/components/common/ReactionStaticEmoji.tsx @@ -0,0 +1,36 @@ +import { RefObject } from 'react'; +import React, { FC, memo } from '../../lib/teact/teact'; +import { getGlobal } from '../../lib/teact/teactn'; + +import { ApiMediaFormat } from '../../api/types'; + +import useMedia from '../../hooks/useMedia'; +import buildClassName from '../../util/buildClassName'; + +import './ReactionStaticEmoji.scss'; + +type OwnProps = { + reaction: string; + ref?: RefObject; + className?: string; +}; + +const ReactionStaticEmoji: FC = ({ + reaction, + ref, + className, +}) => { + const staticIconId = getGlobal().availableReactions?.find((l) => l.reaction === reaction)?.staticIcon?.id; + const mediaData = useMedia(`document${staticIconId}`, !staticIconId, ApiMediaFormat.BlobUrl); + + return ( + + ); +}; + +export default memo(ReactionStaticEmoji); diff --git a/src/components/common/TypingStatus.tsx b/src/components/common/TypingStatus.tsx index 915eb5322..e4a58f076 100644 --- a/src/components/common/TypingStatus.tsx +++ b/src/components/common/TypingStatus.tsx @@ -28,7 +28,7 @@ const TypingStatus: FC = ({ typingStatus, typingUser }) = {renderText(typingUserName)} )} {/* fix for translation "username _is_ typing" */} - {lang(typingStatus.action).replace('{user}', '').trim()} + {lang(typingStatus.action).replace('{user}', '').replace('{emoji}', typingStatus.emoji).trim()}

); diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index b7dc7743f..691003c8b 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -3,35 +3,41 @@ import { ApiMediaFormat } from '../../../api/types'; import * as mediaLoader from '../../../util/mediaLoader'; // @ts-ignore -import MonkeyIdle from '../../../assets/TwoFactorSetupMonkeyIdle.tgs'; +import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs'; // @ts-ignore -import MonkeyTracking from '../../../assets/TwoFactorSetupMonkeyTracking.tgs'; +import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs'; // @ts-ignore -import MonkeyClose from '../../../assets/TwoFactorSetupMonkeyClose.tgs'; +import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs'; // @ts-ignore -import MonkeyPeek from '../../../assets/TwoFactorSetupMonkeyPeek.tgs'; +import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs'; // @ts-ignore -import FoldersAll from '../../../assets/FoldersAll.tgs'; +import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs'; // @ts-ignore -import FoldersNew from '../../../assets/FoldersNew.tgs'; +import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs'; // @ts-ignore -import DiscussionGroups from '../../../assets/DiscussionGroupsDucks.tgs'; +import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; // @ts-ignore -import CameraFlip from '../../../assets/animatedIcons/CameraFlip.tgs'; +import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs'; // @ts-ignore -import HandFilled from '../../../assets/animatedIcons/HandFilled.tgs'; +import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs'; // @ts-ignore -import HandOutline from '../../../assets/animatedIcons/HandOutline.tgs'; +import HandOutline from '../../../assets/tgs/calls/HandOutline.tgs'; // @ts-ignore -import Speaker from '../../../assets/animatedIcons/Speaker.tgs'; +import Speaker from '../../../assets/tgs/calls/Speaker.tgs'; // @ts-ignore -import VoiceAllowTalk from '../../../assets/animatedIcons/VoiceAllowTalk.tgs'; +import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs'; // @ts-ignore -import VoiceMini from '../../../assets/animatedIcons/VoiceMini.tgs'; +import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs'; // @ts-ignore -import VoiceMuted from '../../../assets/animatedIcons/VoiceMuted.tgs'; +import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; // @ts-ignore -import VoiceOutlined from '../../../assets/animatedIcons/VoiceOutlined.tgs'; +import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; +// @ts-ignore +import Peach from '../../../assets/tgs/animatedEmojis/Peach.tgs'; +// @ts-ignore +import Eggplant from '../../../assets/tgs/animatedEmojis/Eggplant.tgs'; +// @ts-ignore +import Cumshot from '../../../assets/tgs/animatedEmojis/Cumshot.tgs'; export const ANIMATED_STICKERS_PATHS = { MonkeyIdle, @@ -49,6 +55,9 @@ export const ANIMATED_STICKERS_PATHS = { VoiceMini, VoiceMuted, VoiceOutlined, + Peach, + Eggplant, + Cumshot, }; export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) { diff --git a/src/components/common/hooks/useAnimatedEmoji.ts b/src/components/common/hooks/useAnimatedEmoji.ts new file mode 100644 index 000000000..1814d1e1d --- /dev/null +++ b/src/components/common/hooks/useAnimatedEmoji.ts @@ -0,0 +1,155 @@ +import { + useCallback, useEffect, useRef, useState, +} from '../../../lib/teact/teact'; +import safePlay from '../../../util/safePlay'; +import { getDispatch } from '../../../lib/teact/teactn'; +import useMedia from '../../../hooks/useMedia'; +import { ActiveEmojiInteraction } from '../../../global/types'; +import useFlag from '../../../hooks/useFlag'; +import { selectLocalAnimatedEmojiEffectByName } from '../../../modules/selectors'; + +const WIDTH = { + large: 160, + medium: 128, + small: 104, +}; +const INTERACTION_BUNCH_TIME = 1000; +const MS_DIVIDER = 1000; +const TIME_DEFAULT = 0; + +export default function useAnimatedEmoji( + size: 'large' | 'medium' | 'small', + chatId?: string, + messageId?: number, + soundId?: string, + activeEmojiInteraction?: ActiveEmojiInteraction, + isOwn?: boolean, + localEffect?: string, + emoji?: string, +) { + const { + interactWithAnimatedEmoji, sendEmojiInteraction, sendWatchingEmojiInteraction, + } = getDispatch(); + + const hasEffect = localEffect || emoji; + const [isAnimationLoaded, markAnimationLoaded] = useFlag(); + + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + + // eslint-disable-next-line no-null/no-null + const audioRef = useRef(null); + + const soundMediaData = useMedia(soundId ? `document${soundId}` : undefined, !soundId); + + const width = WIDTH[size]; + const style = `width: ${width}px; height: ${width}px;`; + + const [playKey, setPlayKey] = useState(String(Math.random())); + const interactions = useRef(undefined); + const startedInteractions = useRef(undefined); + const sendInteractionBunch = useCallback(() => { + const container = ref.current; + + if (!container) return; + + sendEmojiInteraction({ + chatId, + messageId, + localEffect, + emoji, + interactions: interactions.current, + }); + startedInteractions.current = undefined; + interactions.current = undefined; + }, [sendEmojiInteraction, chatId, messageId, localEffect, emoji]); + + const play = useCallback(() => { + setPlayKey(String(Math.random())); + + const audio = audioRef.current; + if (soundMediaData) { + if (audio) { + audio.pause(); + audio.remove(); + } + audioRef.current = new Audio(); + audioRef.current.src = soundMediaData; + safePlay(audioRef.current); + audioRef.current.addEventListener('ended', () => { + audioRef.current = undefined; + }, { once: true }); + } + }, [soundMediaData]); + + const handleClick = useCallback(() => { + play(); + + const container = ref.current; + + if (!hasEffect || !container || !messageId || !chatId) { + return; + } + + const { x, y } = container.getBoundingClientRect(); + + interactWithAnimatedEmoji({ + localEffect, + emoji, + x, + y, + startSize: width, + isReversed: !isOwn, + }); + + if (!interactions.current) { + interactions.current = []; + startedInteractions.current = performance.now(); + setTimeout(sendInteractionBunch, INTERACTION_BUNCH_TIME); + } + + interactions.current.push(startedInteractions.current + ? (performance.now() - startedInteractions.current) / MS_DIVIDER + : TIME_DEFAULT); + }, [ + chatId, emoji, hasEffect, interactWithAnimatedEmoji, isOwn, + localEffect, messageId, play, sendInteractionBunch, width, + ]); + + // Set an end anchor for remote activated interaction + useEffect(() => { + const container = ref.current; + + if (!container || !activeEmojiInteraction) return; + + const { + messageId: selectedMessageId, endX, endY, + } = activeEmojiInteraction; + + if (!endX && !endY && selectedMessageId === messageId) { + const { x, y } = container.getBoundingClientRect(); + + sendWatchingEmojiInteraction({ + chatId, + emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji, + startSize: width, + x, + y, + isReversed: !isOwn, + }); + play(); + } + }, [ + activeEmojiInteraction, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, width, + ]); + + return { + playKey, + ref, + style, + width, + handleClick, + markAnimationLoaded, + isAnimationLoaded, + }; +} diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index 2619792c9..2cfb5eb7e 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -122,6 +122,7 @@ const LeftColumn: FC = ({ return; case SettingsScreens.GeneralChatBackground: + case SettingsScreens.QuickReaction: setSettingsScreen(SettingsScreens.General); return; case SettingsScreens.GeneralChatBackgroundColor: diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index e00f2fc78..d8c50d268 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -301,3 +301,19 @@ .username-link { color: var(--color-links); } + +.settings-quick-reaction { + .ReactionStaticEmoji { + margin-inline-end: 1rem; + width: 1.5rem + } +} + +.SettingsDefaultReaction { + .ReactionStaticEmoji { + width: 1.5rem; + height: 1.5rem; + margin-inline-end: 2rem; + } + +} diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index 8990172d6..afa8d42ad 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -23,6 +23,7 @@ import SettingsPrivacyActiveSessions from './SettingsPrivacyActiveSessions'; import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers'; import SettingsTwoFa from './twoFa/SettingsTwoFa'; import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList'; +import SettingsQuickReaction from './SettingsQuickReaction'; import './Settings.scss'; @@ -180,10 +181,15 @@ const Settings: FC = ({ isActive={isScreenActive || screen === SettingsScreens.GeneralChatBackgroundColor || screen === SettingsScreens.GeneralChatBackground + || screen === SettingsScreens.QuickReaction || isPrivacyScreen || isFoldersScreen} onReset={handleReset} /> ); + case SettingsScreens.QuickReaction: + return ( + + ); case SettingsScreens.Notifications: return ( diff --git a/src/components/left/settings/SettingsGeneral.tsx b/src/components/left/settings/SettingsGeneral.tsx index e31455bfc..84a5025ed 100644 --- a/src/components/left/settings/SettingsGeneral.tsx +++ b/src/components/left/settings/SettingsGeneral.tsx @@ -20,6 +20,7 @@ import Checkbox from '../../ui/Checkbox'; import RadioGroup, { IRadioOption } from '../../ui/RadioGroup'; import SettingsStickerSet from './SettingsStickerSet'; import StickerSetModal from '../../common/StickerSetModal.async'; +import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; type OwnProps = { isActive?: boolean; @@ -38,6 +39,7 @@ type StateProps = )> & { stickerSetIds?: string[]; stickerSetsById?: Record; + defaultReaction?: string; }; const ANIMATION_LEVEL_OPTIONS = [ @@ -60,6 +62,7 @@ const SettingsGeneral: FC = ({ onReset, stickerSetIds, stickerSetsById, + defaultReaction, messageTextSize, animationLevel, messageSendKeyCombo, @@ -189,6 +192,16 @@ const SettingsGeneral: FC = ({

{lang('AccDescrStickers')}

+ {defaultReaction && ( + onScreenSelect(SettingsScreens.QuickReaction)} + > + +
{lang('DoubleTapSetting')}
+
+ )} + ( ]), stickerSetIds: global.stickers.added.setIds, stickerSetsById: global.stickers.setsById, + defaultReaction: global.appConfig?.defaultReaction, }; }, )(SettingsGeneral)); diff --git a/src/components/left/settings/SettingsHeader.tsx b/src/components/left/settings/SettingsHeader.tsx index fbcc028ba..374e6dc7c 100644 --- a/src/components/left/settings/SettingsHeader.tsx +++ b/src/components/left/settings/SettingsHeader.tsx @@ -87,6 +87,8 @@ const SettingsHeader: FC = ({ return

{lang('lng_settings_information')}

; case SettingsScreens.General: return

{lang('General')}

; + case SettingsScreens.QuickReaction: + return

{lang('DoubleTapSetting')}

; case SettingsScreens.Notifications: return

{lang('Notifications')}

; case SettingsScreens.DataStorage: diff --git a/src/components/left/settings/SettingsQuickReaction.tsx b/src/components/left/settings/SettingsQuickReaction.tsx new file mode 100644 index 000000000..a7e06c1ec --- /dev/null +++ b/src/components/left/settings/SettingsQuickReaction.tsx @@ -0,0 +1,65 @@ +import React, { FC, memo, useCallback } from '../../../lib/teact/teact'; +import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; + +import { SettingsScreens } from '../../../types'; +import { ApiAvailableReaction } from '../../../api/types'; + +import useHistoryBack from '../../../hooks/useHistoryBack'; + +import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; +import RadioGroup from '../../ui/RadioGroup'; + +type OwnProps = { + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; +}; + +type StateProps = { + availableReactions?: ApiAvailableReaction[]; + selectedReaction?: string; +}; + +const SettingsQuickReaction: FC = ({ + isActive, + onReset, + onScreenSelect, + availableReactions, + selectedReaction, +}) => { + const { setDefaultReaction } = getDispatch(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General); + + const options = availableReactions?.filter((l) => !l.isInactive).map((l) => { + return { + label: <>{l.title}, + value: l.reaction, + }; + }) || []; + + const handleChange = useCallback((reaction: string) => { + setDefaultReaction({ reaction }); + }, [setDefaultReaction]); + + return ( +
+ +
+ ); +}; + +export default memo(withGlobal( + (global) => { + const { availableReactions, appConfig } = global; + + return { + availableReactions, + selectedReaction: appConfig?.defaultReaction, + }; + }, +)(SettingsQuickReaction)); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 43b20afe4..f7dfa2c1f 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -108,12 +108,14 @@ const Main: FC = ({ loadTopInlineBots, loadEmojiKeywords, loadCountryList, + loadAvailableReactions, loadStickerSets, loadAddedStickers, loadFavoriteStickers, ensureTimeFormat, openStickerSetShortName, checkVersionNotification, + loadAppConfig, } = getDispatch(); const isSynced = Boolean(lastSyncTime); @@ -127,6 +129,8 @@ const Main: FC = ({ useEffect(() => { if (lastSyncTime) { updateIsOnline(true); + loadAppConfig(); + loadAvailableReactions(); loadAnimatedEmojis(); loadNotificationSettings(); loadNotificationExceptions(); @@ -135,7 +139,7 @@ const Main: FC = ({ } }, [ lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings, - loadTopInlineBots, updateIsOnline, + loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, ]); // Language-based API calls diff --git a/src/components/middle/EmojiInteractionAnimation.async.tsx b/src/components/middle/EmojiInteractionAnimation.async.tsx new file mode 100644 index 000000000..dbfb73caf --- /dev/null +++ b/src/components/middle/EmojiInteractionAnimation.async.tsx @@ -0,0 +1,15 @@ +import React, { FC, memo } from '../../lib/teact/teact'; +import { OwnProps } from './EmojiInteractionAnimation'; +import { Bundles } from '../../util/moduleLoader'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const EmojiInteractionAnimationAsync: FC = (props) => { + const { emojiInteraction } = props; + const EmojiInteractionAnimation = useModuleLoader(Bundles.Extra, 'EmojiInteractionAnimation', !emojiInteraction); + + // eslint-disable-next-line react/jsx-props-no-spreading + return EmojiInteractionAnimation ? : undefined; +}; + +export default memo(EmojiInteractionAnimationAsync); diff --git a/src/components/middle/EmojiInteractionAnimation.scss b/src/components/middle/EmojiInteractionAnimation.scss new file mode 100644 index 000000000..955801fee --- /dev/null +++ b/src/components/middle/EmojiInteractionAnimation.scss @@ -0,0 +1,89 @@ +.EmojiInteractionAnimation { + --start-x: 0; + --start-y: 0; + --scale: 0; + --end-scale: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1000; + + @keyframes hide-reaction-reversed { + from { + transform: translate(100%, -50%) scaleX(-1) scale(1); + } + + to { + left: var(--end-x, var(--start-x)); + top: var(--end-y, var(--start-y)); + transform: translate(50%, 0) scale(var(--end-scale, 0)); + } + } + + @keyframes show-reaction-reversed { + from { + transform: translate(50%, 0) scaleX(-1) scale(var(--scale, 0)); + } + + to { + transform: translate(100%, -50%) scaleX(-1) scale(1); + } + } + + + @keyframes hide-reaction { + from { + transform: translate(-50%, -50%) scale(1); + } + + to { + left: var(--end-x, var(--start-x)); + top: var(--end-y, var(--start-y)); + transform: translate(0, 0) scale(var(--end-scale, 0)); + } + } + + @keyframes show-reaction { + from { + transform: translate(0, 0) scale(var(--scale, 0)); + } + + to { + transform: translate(-50%, -50%) scale(1); + } + } + + .AnimatedSticker { + position: absolute; + top: var(--start-y); + left: var(--start-x); + transform: scale(var(--scale), 0); + transform-origin: left top; + + transition: 0.25s cubic-bezier(.29,.81,.27,.99) opacity; + } + + &.reversed .AnimatedSticker { + transform: scale(var(--scale), 0) scaleX(-1); + } + + &.playing .AnimatedSticker { + animation: show-reaction forwards 0.25s cubic-bezier(.29,.81,.27,.99); + } + + &.reversed.playing .AnimatedSticker { + animation: show-reaction-reversed forwards 0.25s cubic-bezier(.29,.81,.27,.99); + } + + &.hiding .AnimatedSticker { + opacity: 0; + animation: hide-reaction forwards 0.25s cubic-bezier(.29,.81,.27,.99); + } + + &.reversed.hiding .AnimatedSticker { + animation: hide-reaction-reversed forwards 0.25s cubic-bezier(.29,.81,.27,.99); + } +} diff --git a/src/components/middle/EmojiInteractionAnimation.tsx b/src/components/middle/EmojiInteractionAnimation.tsx new file mode 100644 index 000000000..eb0782e25 --- /dev/null +++ b/src/components/middle/EmojiInteractionAnimation.tsx @@ -0,0 +1,122 @@ +import React, { + FC, memo, useCallback, useEffect, useState, +} from '../../lib/teact/teact'; +import { getDispatch, withGlobal } from '../../lib/teact/teactn'; + +import { ActiveEmojiInteraction } from '../../global/types'; +import { ApiMediaFormat } from '../../api/types'; + +import useFlag from '../../hooks/useFlag'; +import useMedia from '../../hooks/useMedia'; +import buildClassName from '../../util/buildClassName'; +import { + selectAnimatedEmojiEffect, +} from '../../modules/selectors'; +import { REM } from '../common/helpers/mediaDimensions'; +import getAnimationData, { ANIMATED_STICKERS_PATHS } from '../common/helpers/animatedAssets'; + +import AnimatedSticker from '../common/AnimatedSticker'; + +import './EmojiInteractionAnimation.scss'; + +export type OwnProps = { + emojiInteraction: ActiveEmojiInteraction; +}; + +type StateProps = { + effectAnimationId?: string; + localEffectAnimation?: string; + isReversed?: boolean; +}; + +const HIDE_ANIMATION_DURATION = 250; +const PLAYING_DURATION = 3000; +const END_SIZE = 1.125 * REM; +const EFFECT_SIZE = 240; + +const EmojiInteractionAnimation: FC = ({ + emojiInteraction, + effectAnimationId, + localEffectAnimation, + isReversed, +}) => { + const { stopActiveEmojiInteraction } = getDispatch(); + + const [isHiding, startHiding] = useFlag(false); + const [isPlaying, startPlaying] = useFlag(false); + + const stop = useCallback(() => { + startHiding(); + setTimeout(() => { + stopActiveEmojiInteraction(); + }, HIDE_ANIMATION_DURATION); + }, [startHiding, stopActiveEmojiInteraction]); + + useEffect(() => { + document.addEventListener('touchstart', stop); + document.addEventListener('touchmove', stop); + document.addEventListener('mousedown', stop); + document.addEventListener('wheel', stop); + + return () => { + document.removeEventListener('touchstart', stop); + document.removeEventListener('touchmove', stop); + document.removeEventListener('mousedown', stop); + document.removeEventListener('wheel', stop); + }; + }, [stop]); + + useEffect(() => { + setTimeout(stop, PLAYING_DURATION); + }, [stop]); + + const mediaDataEffect = useMedia(`sticker${effectAnimationId}`, !effectAnimationId, ApiMediaFormat.Lottie); + + const [localEffectAnimationData, setLocalEffectAnimationData] = useState(); + useEffect(() => { + if (localEffectAnimation) { + getAnimationData(localEffectAnimation as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => { + setLocalEffectAnimationData(data); + }); + } + }, [localEffectAnimation]); + + const scale = (emojiInteraction.startSize || 0) / EFFECT_SIZE; + const endScale = END_SIZE / EFFECT_SIZE; + + return ( +
+ +
+ ); +}; + +export default memo(withGlobal( + (global, { emojiInteraction }): StateProps => { + const animatedEffect = emojiInteraction.animatedEffect !== undefined + && selectAnimatedEmojiEffect(global, emojiInteraction.animatedEffect); + return { + effectAnimationId: animatedEffect ? animatedEffect.id : undefined, + localEffectAnimation: !animatedEffect && emojiInteraction.animatedEffect + && Object.keys(ANIMATED_STICKERS_PATHS).includes(emojiInteraction.animatedEffect) + ? emojiInteraction.animatedEffect : undefined, + isReversed: emojiInteraction.isReversed, + }; + }, +)(EmojiInteractionAnimation)); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 55c809912..c1d8c67c6 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -47,6 +47,7 @@ import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll import renderText from '../common/helpers/renderText'; import useLang from '../../hooks/useLang'; import useWindowSize from '../../hooks/useWindowSize'; +import useInterval from '../../hooks/useInterval'; import Loading from '../ui/Loading'; import MessageListContent from './MessageListContent'; @@ -92,6 +93,7 @@ type StateProps = { lastSyncTime?: number; }; +const MESSAGE_REACTIONS_POLLING_INTERVAL = 15 * 1000; const BOTTOM_THRESHOLD = 20; const UNREAD_DIVIDER_TOP = 10; const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60; @@ -135,7 +137,9 @@ const MessageList: FC = ({ lastSyncTime, withBottomShift, }) => { - const { loadViewportMessages, setScrollOffset, loadSponsoredMessages } = getDispatch(); + const { + loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, + } = getDispatch(); // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); @@ -204,6 +208,17 @@ const MessageList: FC = ({ return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current); }, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]); + useInterval(() => { + if (!messageIds || !messagesById) { + return; + } + const ids = messageIds.filter((l) => messagesById[l]?.reactions); + + if (!ids.length) return; + + loadMessageReactions({ chatId, ids }); + }, MESSAGE_REACTIONS_POLLING_INTERVAL); + const loadMoreAround = useMemo(() => { if (type !== 'thread') { return undefined; @@ -520,6 +535,7 @@ const MessageList: FC = ({ isViewportNewest={Boolean(isViewportNewest)} isUnread={Boolean(firstUnreadId)} withUsers={withUsers} + areReactionsInMeta={isPrivate} noAvatars={noAvatars} containerRef={containerRef} anchorIdRef={anchorIdRef} diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index f817e9602..770954890 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -34,6 +34,7 @@ interface OwnProps { threadId: number; type: MessageListType; isReady: boolean; + areReactionsInMeta: boolean; isScrollingRef: { current: boolean | undefined }; isScrollPatchNeededRef: { current: boolean | undefined }; threadTopMessageId: number | undefined; @@ -53,6 +54,7 @@ const MessageListContent: FC = ({ isViewportNewest, isUnread, withUsers, + areReactionsInMeta, noAvatars, containerRef, anchorIdRef, @@ -188,6 +190,7 @@ const MessageListContent: FC = ({ noAvatars={noAvatars} withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)} withSenderName={position.isFirstInGroup && withUsers && !isOwn} + areReactionsInMeta={areReactionsInMeta} threadId={threadId} messageListType={type} noComments={hasLinkedChat === false} diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index c247a8004..84d244447 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -4,7 +4,11 @@ import React, { import { getDispatch, withGlobal } from '../../lib/teact/teactn'; import { ApiChatBannedRights, MAIN_THREAD_ID } from '../../api/types'; -import { MessageListType, MessageList as GlobalMessageList } from '../../global/types'; +import { + MessageListType, + MessageList as GlobalMessageList, + ActiveEmojiInteraction, +} from '../../global/types'; import { ThemeKey } from '../../types'; import { @@ -67,6 +71,8 @@ import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async'; import PaymentModal from '../payment/PaymentModal.async'; import ReceiptModal from '../payment/ReceiptModal.async'; import SeenByModal from '../common/SeenByModal.async'; +import EmojiInteractionAnimation from './EmojiInteractionAnimation.async'; +import ReactorListModal from './ReactorListModal.async'; import './MiddleColumn.scss'; @@ -94,6 +100,7 @@ type StateProps = { isPaymentModalOpen?: boolean; isReceiptModalOpen?: boolean; isSeenByModalOpen: boolean; + isReactorListModalOpen: boolean; animationLevel?: number; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; @@ -102,6 +109,7 @@ type StateProps = { canSubscribe?: boolean; canStartBot?: boolean; canRestartBot?: boolean; + activeEmojiInteraction?: ActiveEmojiInteraction; }; const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined; @@ -134,6 +142,7 @@ const MiddleColumn: FC = ({ isPaymentModalOpen, isReceiptModalOpen, isSeenByModalOpen, + isReactorListModalOpen, animationLevel, shouldSkipHistoryAnimations, currentTransitionKey, @@ -141,6 +150,7 @@ const MiddleColumn: FC = ({ canSubscribe, canStartBot, canRestartBot, + activeEmojiInteraction, }) => { const { openChat, @@ -484,6 +494,7 @@ const MiddleColumn: FC = ({ onClose={clearReceipt} /> +
)} @@ -507,6 +518,9 @@ const MiddleColumn: FC = ({ onUnpin={handleUnpinAllMessages} /> )} + {activeEmojiInteraction && ( + + )}
); }; @@ -520,7 +534,7 @@ export default memo(withGlobal( const { messageLists } = global.messages; const currentMessageList = selectCurrentMessageList(global); - const { isLeftColumnShown, chats: { listIds } } = global; + const { isLeftColumnShown, chats: { listIds }, activeEmojiInteraction } = global; const state: StateProps = { theme, @@ -535,8 +549,10 @@ export default memo(withGlobal( isPaymentModalOpen: global.payment.isPaymentModalOpen, isReceiptModalOpen: Boolean(global.payment.receipt), isSeenByModalOpen: Boolean(global.seenByModal), + isReactorListModalOpen: Boolean(global.reactorModal), animationLevel: global.settings.byKey.animationLevel, currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1), + activeEmojiInteraction, }; if (!currentMessageList || !listIds.active) { diff --git a/src/components/middle/ReactorListModal.async.tsx b/src/components/middle/ReactorListModal.async.tsx new file mode 100644 index 000000000..8d221cbb6 --- /dev/null +++ b/src/components/middle/ReactorListModal.async.tsx @@ -0,0 +1,15 @@ +import React, { FC, memo } from '../../lib/teact/teact'; +import { OwnProps } from './ReactorListModal'; +import { Bundles } from '../../util/moduleLoader'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const ReactorListModalAsync: FC = (props) => { + const { isOpen } = props; + const ReactorListModal = useModuleLoader(Bundles.Extra, 'ReactorListModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ReactorListModal ? : undefined; +}; + +export default memo(ReactorListModalAsync); diff --git a/src/components/middle/ReactorListModal.scss b/src/components/middle/ReactorListModal.scss new file mode 100644 index 000000000..a66a51df6 --- /dev/null +++ b/src/components/middle/ReactorListModal.scss @@ -0,0 +1,38 @@ +.ReactorListModal { + --color-reaction: var(--color-message-reaction); + --hover-color-reaction: var(--color-message-reaction-hover); + --accent-color: var(--color-primary); + + .modal-content { + overflow: hidden; + } + + .Reactions { + margin-bottom: 0.5rem; + } + + .reaction-filter-emoji { + margin-right: 0.25rem; + } + + .reactor-list { + max-height: 400px; + overflow: auto; + overflow-x: hidden; + } + + .reactors-list-item { + margin: 0; + } + + .reactors-list-item .ListItem-button { + display: flex; + align-items: center; + } + + .reactors-list-emoji { + width: 1.5rem; + height: 1.5rem; + margin-inline-start: auto; + } +} diff --git a/src/components/middle/ReactorListModal.tsx b/src/components/middle/ReactorListModal.tsx new file mode 100644 index 000000000..660dfeaee --- /dev/null +++ b/src/components/middle/ReactorListModal.tsx @@ -0,0 +1,203 @@ +import React, { + FC, useCallback, memo, useMemo, useEffect, useState, useRef, +} from '../../lib/teact/teact'; +import { getDispatch, getGlobal, withGlobal } from '../../lib/teact/teactn'; + +import { ApiMessage } from '../../api/types'; +import { LoadMoreDirection } from '../../types'; + +import useLang from '../../hooks/useLang'; +import { selectChatMessage } from '../../modules/selectors'; +import useInfiniteScroll from '../../hooks/useInfiniteScroll'; +import { getUserFullName } from '../../modules/helpers'; +import renderText from '../common/helpers/renderText'; +import useFlag from '../../hooks/useFlag'; +import buildClassName from '../../util/buildClassName'; +import { formatIntegerCompact } from '../../util/textFormat'; +import { unique } from '../../util/iteratees'; + +import InfiniteScroll from '../ui/InfiniteScroll'; +import Modal from '../ui/Modal'; +import Button from '../ui/Button'; +import Avatar from '../common/Avatar'; +import ListItem from '../ui/ListItem'; +import ReactionStaticEmoji from '../common/ReactionStaticEmoji'; +import Loading from '../ui/Loading'; + +import './ReactorListModal.scss'; + +const MIN_REACTIONS_COUNT_FOR_FILTERS = 10; + +export type OwnProps = { + isOpen: boolean; +}; + +export type StateProps = Pick & { + chatId?: string; + messageId?: number; +}; + +const ReactorListModal: FC = ({ + isOpen, + reactors, + reactions, + chatId, + messageId, + seenByUserIds, +}) => { + const { + loadReactors, + closeReactorListModal, + openChat, + } = getDispatch(); + + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + + const lang = useLang(); + const [isClosing, startClosing, stopClosing] = useFlag(false); + const [chosenTab, setChosenTab] = useState(undefined); + const canShowFilters = reactors && reactions && reactors.count >= MIN_REACTIONS_COUNT_FOR_FILTERS + && reactions.results.length > 1; + const chatIdRef = useRef(); + + useEffect(() => { + if (isClosing && !isOpen) { + stopClosing(); + setChosenTab(undefined); + } + }, [isClosing, isOpen, stopClosing]); + + const handleCloseAnimationEnd = useCallback(() => { + if (chatIdRef.current) { + openChat({ id: chatIdRef.current }); + } + closeReactorListModal(); + }, [closeReactorListModal, openChat]); + + const handleClose = useCallback(() => { + startClosing(); + }, [startClosing]); + + const handleClick = useCallback((userId: string) => { + chatIdRef.current = userId; + handleClose(); + }, [handleClose]); + + const handleLoadMore = useCallback(() => { + loadReactors({ + chatId, + messageId, + }); + }, [chatId, loadReactors, messageId]); + + const allReactions = useMemo(() => { + return reactors?.reactions ? unique(reactors.reactions.map((l) => l.reaction)) : []; + }, [reactors?.reactions]); + + const userIds = useMemo(() => { + if (chosenTab) { + return reactors?.reactions.filter((l) => l.reaction === chosenTab).map((l) => l.userId); + } + return unique(reactors?.reactions.map((l) => l.userId).concat(seenByUserIds || []) || []); + }, [chosenTab, reactors?.reactions, seenByUserIds]); + + const [viewportIds, getMore] = useInfiniteScroll( + handleLoadMore, userIds, reactors && reactors.nextOffset === undefined, + ); + + useEffect(() => { + getMore?.({ direction: LoadMoreDirection.Backwards }); + }, [getMore]); + + return ( + + {canShowFilters && ( +
+ + {allReactions.map((reaction) => { + const count = reactions?.results.find((l) => l.reaction === reaction)?.count; + return ( + + ); + })} +
+ )} + +
+ {viewportIds?.length ? ( + + {viewportIds?.map( + (userId) => { + const user = usersById[userId]; + const fullName = getUserFullName(user); + const reaction = reactors?.reactions.find((l) => l.userId === userId)?.reaction; + return ( + handleClick(userId)} + > + +
+

{fullName && renderText(fullName)}

+
+ {reaction && } +
+ ); + }, + )} +
+ ) : } +
+ +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { chatId, messageId } = global.reactorModal || {}; + const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined; + + return { + chatId, + messageId, + reactions: message?.reactions, + reactors: message?.reactors, + seenByUserIds: message?.seenByUserIds, + }; + }, +)(ReactorListModal)); diff --git a/src/components/middle/helpers/calculateMiddleFooterTransforms.ts b/src/components/middle/helpers/calculateMiddleFooterTransforms.ts index af4f31919..7a4eee33b 100644 --- a/src/components/middle/helpers/calculateMiddleFooterTransforms.ts +++ b/src/components/middle/helpers/calculateMiddleFooterTransforms.ts @@ -7,8 +7,8 @@ import { const REM = 16; // px const MAX_TOOLBAR_WIDTH = 32 * REM; const MAX_MESSAGES_LIST_WIDTH = 45.5 * REM; -const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM; -const MIN_LEFT_COLUMN_WIDTH = 18 * REM; +export const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM; +export const MIN_LEFT_COLUMN_WIDTH = 18 * REM; const UNPIN_BUTTON_WIDTH = 16.125 * REM; export default function calculateMiddleFooterTransforms(windowWidth: number, canPost?: boolean) { diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index d2e6c3b1c..6925ef7a5 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -4,7 +4,7 @@ import React, { import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn'; import { MessageListType } from '../../../global/types'; -import { ApiMessage } from '../../../api/types'; +import { ApiAvailableReaction, ApiMessage } from '../../../api/types'; import { IAlbum, IAnchorPosition } from '../../../types'; import { selectActiveDownloadIds, @@ -13,12 +13,16 @@ import { selectCurrentMessageList, selectIsMessageProtected, } from '../../../modules/selectors'; -import { isChatGroup, isOwnMessage } from '../../../modules/helpers'; -import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX } from '../../../config'; +import { + isActionMessage, isChatChannel, + isChatGroup, isOwnMessage, areReactionsEmpty, isUserId, +} from '../../../modules/helpers'; +import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { getDayStartAt } from '../../../util/dateFormat'; import { copyTextToClipboard } from '../../../util/clipboard'; import useShowTransition from '../../../hooks/useShowTransition'; import useFlag from '../../../hooks/useFlag'; +import { REM } from '../../common/helpers/mediaDimensions'; import DeleteMessageModal from '../../common/DeleteMessageModal'; import ReportMessageModal from '../../common/ReportMessageModal'; @@ -26,6 +30,8 @@ import PinMessageModal from '../../common/PinMessageModal'; import MessageContextMenu from './MessageContextMenu'; import CalendarModal from '../../common/CalendarModal'; +const START_SIZE = 2 * REM; + export type OwnProps = { isOpen: boolean; chatUsername?: string; @@ -38,11 +44,15 @@ export type OwnProps = { }; type StateProps = { + availableReactions?: ApiAvailableReaction[]; noOptions?: boolean; canSendNow?: boolean; canReschedule?: boolean; canReply?: boolean; canPin?: boolean; + canShowReactionsCount?: boolean; + canShowReactionList?: boolean; + canRemoveReaction?: boolean; canUnpin?: boolean; canDelete?: boolean; canReport?: boolean; @@ -51,14 +61,18 @@ type StateProps = { canFaveSticker?: boolean; canUnfaveSticker?: boolean; canCopy?: boolean; + isPrivate?: boolean; + hasFullInfo?: boolean; canCopyLink?: boolean; canSelect?: boolean; canDownload?: boolean; activeDownloads: number[]; canShowSeenBy?: boolean; + enabledReactions?: string[]; }; const ContextMenuContainer: FC = ({ + availableReactions, isOpen, messageListType, chatUsername, @@ -69,13 +83,19 @@ const ContextMenuContainer: FC = ({ onCloseAnimationEnd, noOptions, canSendNow, + hasFullInfo, canReschedule, canReply, canPin, canUnpin, canDelete, canReport, + canShowReactionsCount, + canShowReactionList, + canRemoveReaction, canEdit, + enabledReactions, + isPrivate, canForward, canFaveSticker, canUnfaveSticker, @@ -100,6 +120,10 @@ const ContextMenuContainer: FC = ({ cancelMessageMediaDownload, loadSeenBy, openSeenByModal, + sendReaction, + openReactorListModal, + loadFullChat, + loadReactors, } = getDispatch(); const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false); @@ -115,7 +139,26 @@ const ContextMenuContainer: FC = ({ } }, [loadSeenBy, isOpen, message.chatId, message.id, canShowSeenBy]); + useEffect(() => { + if (canShowReactionsCount && isOpen) { + loadReactors({ chatId: message.chatId, messageId: message.id }); + } + }, [canShowReactionsCount, isOpen, loadReactors, message.chatId, message.id]); + + useEffect(() => { + if (!hasFullInfo && !isPrivate && isOpen) { + loadFullChat({ chatId: message.chatId }); + } + }, [hasFullInfo, isOpen, isPrivate, loadFullChat, message.chatId]); + const seenByRecentUsers = useMemo(() => { + if (message.reactions?.recentReactions?.length) { + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + + return message.reactions?.recentReactions?.slice(0, 3).map(({ userId }) => usersById[userId]).filter(Boolean); + } + if (!message.seenByUserIds) { return undefined; } @@ -123,7 +166,7 @@ const ContextMenuContainer: FC = ({ // No need for expensive global updates on users, so we avoid them const usersById = getGlobal().users.byId; return message.seenByUserIds?.slice(0, 3).map((id) => usersById[id]).filter(Boolean); - }, [message.seenByUserIds]); + }, [message.reactions?.recentReactions, message.seenByUserIds]); const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id)) : activeDownloads.includes(message.id); @@ -231,6 +274,11 @@ const ContextMenuContainer: FC = ({ openSeenByModal({ chatId: message.chatId, messageId: message.id }); }, [closeMenu, message.chatId, message.id, openSeenByModal]); + const handleOpenReactorListModal = useCallback(() => { + closeMenu(); + openReactorListModal({ chatId: message.chatId, messageId: message.id }); + }, [closeMenu, openReactorListModal, message.chatId, message.id]); + const handleRescheduleMessage = useCallback((date: Date) => { rescheduleMessage({ chatId: message.chatId, @@ -255,6 +303,13 @@ const ContextMenuContainer: FC = ({ closeMenu(); }, [album, message, closeMenu, isDownloading, cancelMessageMediaDownload, downloadMessageMedia]); + const handleSendReaction = useCallback((reaction: string | undefined, x: number, y: number) => { + sendReaction({ + chatId: message.chatId, messageId: message.id, reaction, x, y, startSize: START_SIZE, + }); + closeMenu(); + }, [closeMenu, message.chatId, message.id, sendReaction]); + const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]); if (noOptions) { @@ -269,9 +324,15 @@ const ContextMenuContainer: FC = ({ return (
= ({ onCopyLink={handleCopyLink} onDownload={handleDownloadClick} onShowSeenBy={handleOpenSeenByModal} + onSendReaction={handleSendReaction} + onShowReactors={handleOpenReactorListModal} /> ( } = (threadId && selectAllowedMessageActions(global, message, threadId)) || {}; const isPinned = messageListType === 'pinned'; const isScheduled = messageListType === 'scheduled'; + const isChannel = chat && isChatChannel(chat); const canShowSeenBy = Boolean(chat && isChatGroup(chat) && isOwnMessage(message) && chat.membersCount && chat.membersCount < SEEN_BY_MEMBERS_CHAT_MAX && message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE); + const isPrivate = chat && isUserId(chat.id); + const isAction = isActionMessage(message); + const canShowReactionsCount = !isChannel && !isScheduled && !isAction && !isPrivate && message.reactions + && !areReactionsEmpty(message.reactions) && message.reactions.canSeeList; + const canRemoveReaction = isPrivate && message.reactions?.results?.some((l) => l.isChosen); const isProtected = selectIsMessageProtected(global, message); return { + availableReactions: global.availableReactions, noOptions, canSendNow: isScheduled, canReschedule: isScheduled, @@ -388,6 +458,12 @@ export default memo(withGlobal( canDownload: !isProtected && canDownload, activeDownloads, canShowSeenBy, + enabledReactions: chat?.fullInfo?.enabledReactions, + isPrivate, + hasFullInfo: Boolean(chat?.fullInfo), + canShowReactionsCount, + canShowReactionList: !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID, + canRemoveReaction, }; }, )(ContextMenuContainer)); diff --git a/src/components/middle/message/Message.scss b/src/components/middle/message/Message.scss index 5d9a2f733..f57d730d3 100644 --- a/src/components/middle/message/Message.scss +++ b/src/components/middle/message/Message.scss @@ -7,6 +7,8 @@ --background-color: var(--color-background); --hover-color: var(--color-reply-hover); + --color-reaction: var(--color-message-reaction); + --hover-color-reaction: var(--color-message-reaction-hover); --active-color: var(--color-reply-active); --max-width: 29rem; --accent-color: var(--color-primary); @@ -53,6 +55,42 @@ } } + .quick-reaction { + cursor: pointer; + position: absolute; + right: -0.5rem; + bottom: -0.375rem; + display: flex; + align-items: center; + justify-content: center; + transform: scale(1); + opacity: 0; + transition: transform 0.2s ease-out, opacity 0.2s ease-out; + transition-delay: 0.2s; + + &.visible { + opacity: 1 !important; + } + + &:hover { + transition-delay: unset; + transform: scale(1.4); + } + + .ReactionStaticEmoji { + width: 1.125rem; + } + } + + &.last-in-list .quick-reaction:hover { + transform: translateY(-0.1875rem) scale(1.4); + } + + &.own .quick-reaction { + right: auto; + left: -0.5rem; + } + &.last-in-group { margin-bottom: 0.625rem; } @@ -82,6 +120,12 @@ } } + &.has-active-reaction { + .message-content-wrapper { + z-index: 1; + } + } + &:not(.own) { padding-left: 2.5rem; @@ -102,6 +146,8 @@ flex-direction: row-reverse; --background-color: var(--color-background-own); --hover-color: var(--color-reply-own-hover); + --color-reaction: var(--color-message-reaction-own); + --hover-color-reaction: var(--color-message-reaction-hover-own); --active-color: var(--color-reply-own-active); --max-width: 30rem; --accent-color: var(--color-accent-own); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 0b64993ed..30544203a 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -8,14 +8,14 @@ import React, { } from '../../../lib/teact/teact'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; -import { MessageListType } from '../../../global/types'; +import { ActiveEmojiInteraction, ActiveReaction, MessageListType } from '../../../global/types'; import { ApiMessage, ApiMessageOutgoingStatus, ApiUser, ApiChat, ApiSticker, - ApiThreadInfo, + ApiThreadInfo, ApiAvailableReaction, } from '../../../api/types'; import { AudioOrigin, FocusDirection, IAlbum, ISettings, @@ -45,7 +45,13 @@ import { selectAllowedMessageActions, selectIsDownloading, selectThreadInfo, + selectAnimatedEmojiEffect, + selectAnimatedEmojiSound, + selectMessageIdsByGroupId, + selectLocalAnimatedEmoji, selectIsMessageProtected, + selectLocalAnimatedEmojiEffect, + selectDefaultReaction, } from '../../../modules/selectors'; import { getMessageContent, @@ -60,6 +66,7 @@ import { getMessageSingleEmoji, getSenderTitle, getUserColorKey, + areReactionsEmpty, } from '../../../modules/helpers'; import buildClassName from '../../../util/buildClassName'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; @@ -98,6 +105,9 @@ import Album from './Album'; import RoundVideo from './RoundVideo'; import InlineButtons from './InlineButtons'; import CommentButton from './CommentButton'; +import Reactions from './Reactions'; +import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; +import LocalAnimatedEmoji from '../../common/LocalAnimatedEmoji'; import './Message.scss'; @@ -119,6 +129,7 @@ type OwnProps = noAvatars?: boolean; withAvatar?: boolean; withSenderName?: boolean; + areReactionsInMeta?: boolean; threadId: number; messageListType: MessageListType; noComments: boolean; @@ -136,6 +147,7 @@ type StateProps = { isThreadTop?: boolean; shouldHideReply?: boolean; replyMessage?: ApiMessage; + reactionsMessage?: ApiMessage; replyMessageSender?: ApiUser | ApiChat; outgoingStatus?: ApiMessageOutgoingStatus; uploadProgress?: number; @@ -153,6 +165,10 @@ type StateProps = { highlight?: string; isSingleEmoji?: boolean; animatedEmoji?: ApiSticker; + localSticker?: string; + localEffect?: string; + animatedEmojiEffect?: ApiSticker; + animatedEmojiSoundId?: string; isInSelectMode?: boolean; isSelected?: boolean; isGroupSelected?: boolean; @@ -164,10 +180,13 @@ type StateProps = { shouldLoopStickers?: boolean; autoLoadFileMaxSizeMb: number; threadInfo?: ApiThreadInfo; + defaultReaction?: string; + activeReaction?: ActiveReaction; + activeEmojiInteraction?: ActiveEmojiInteraction; + availableReactions?: ApiAvailableReaction[]; }; const NBSP = '\u00A0'; -const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover'; // eslint-disable-next-line max-len const APPENDIX_OWN = { __html: '' }; // eslint-disable-next-line max-len @@ -185,6 +204,8 @@ const Message: FC = ({ noAvatars, withAvatar, withSenderName, + areReactionsInMeta, + reactionsMessage, noComments, appearanceOrder, isFirstInGroup, @@ -216,10 +237,18 @@ const Message: FC = ({ lastSyncTime, highlight, animatedEmoji, + localSticker, + localEffect, + animatedEmojiEffect, + animatedEmojiSoundId, isInSelectMode, isSelected, isGroupSelected, threadId, + defaultReaction, + activeReaction, + activeEmojiInteraction, + availableReactions, messageListType, isPinnedList, isDownloading, @@ -239,6 +268,8 @@ const Message: FC = ({ const ref = useRef(null); // eslint-disable-next-line no-null/no-null const bottomMarkerRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const contentRef = useRef(null); const lang = useLang(); @@ -288,6 +319,8 @@ const Message: FC = ({ const { text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, } = getMessageContent(message); + const hasReactionButtons = !areReactionsInMeta && reactionsMessage?.reactions + && !areReactionsEmpty(reactionsMessage.reactions); const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape)); const isContextMenuShown = contextMenuPosition !== undefined; const signature = ( @@ -308,6 +341,9 @@ const Message: FC = ({ )); const avatarPeer = forwardInfo && (isChatWithSelf || isRepliesChat || !sender) ? originSender : sender; const senderPeer = forwardInfo ? originSender : sender; + const hasAnimatedEmoji = localSticker || animatedEmoji; + const areReactionsOutside = hasReactionButtons + && (asForwarded || customShape || ((photo || video || hasAnimatedEmoji) && !textParts)); const selectMessage = useCallback((e?: React.MouseEvent, groupedId?: string) => { toggleMessageSelection({ @@ -324,7 +360,12 @@ const Message: FC = ({ handleContextMenu, handleDoubleClick, handleContentDoubleClick, + handleMouseMove, + handleSendQuickReaction, + handleMouseLeave, isSwiped, + isQuickReactionVisible, + handleDocumentGroupMouseEnter, } = useOuterHandlers( selectMessage, ref, @@ -335,6 +376,11 @@ const Message: FC = ({ Boolean(isProtected), onContextMenu, handleBeforeContextMenu, + chatId, + isContextMenuShown, + contentRef, + isOwn, + isInDocumentGroup && !isLastInDocumentGroup, ); const { @@ -395,6 +441,7 @@ const Message: FC = ({ Boolean(message.inlineButtons) && 'has-inline-buttons', isSwiped && 'is-swiped', transitionClassNames, + Boolean(activeReaction) && 'has-active-reaction', ); const contentClassName = buildContentClassName(message, { hasReply, @@ -410,6 +457,7 @@ const Message: FC = ({ threadInfo && (!isInDocumentGroup || isLastInDocumentGroup) && messageListType === 'thread' && !noComments ); const withAppendix = contentClassName.includes('has-appendix'); + const withQuickReaction = !IS_TOUCH_ENV && defaultReaction && (!isInDocumentGroup || isLastInDocumentGroup); useEnsureMessage( isRepliesChat && message.replyToChatId ? message.replyToChatId : chatId, @@ -474,6 +522,21 @@ const Message: FC = ({ ); } + function renderMeta(withReactionOffset = false) { + return ( + + ); + } + function renderContent() { const className = buildClassName( 'content-inner', @@ -482,7 +545,7 @@ const Message: FC = ({ noMediaCorners && 'no-media-corners', ); const hasCustomAppendix = isLastInGroup && !textParts && !asForwarded && !hasThread; - const shouldInlineMeta = !webPage && !animatedEmoji && textParts; + const shouldInlineMeta = !webPage && !hasAnimatedEmoji && textParts; const textContentClass = buildClassName( 'text-content', shouldInlineMeta && 'with-meta', @@ -513,10 +576,31 @@ const Message: FC = ({ {animatedEmoji && ( + )} + {localSticker && ( + )} {isAlbum && ( @@ -605,19 +689,25 @@ const Message: FC = ({ {poll && ( )} - {!animatedEmoji && textParts && ( + {!hasAnimatedEmoji && textParts && (

{textParts} {shouldInlineMeta && ( - + <> + {hasReactionButtons && !areReactionsOutside ? ( + + ) + : renderMeta()} + )}

)} + {webPage && ( = ({ onContextMenu={handleContextMenu} onDoubleClick={handleDoubleClick} onMouseEnter={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseEnter : undefined} - onMouseLeave={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseLeave : undefined} + onMouseMove={withQuickReaction ? handleMouseMove : undefined} + onMouseLeave={(withQuickReaction || (isInDocumentGroup && !isLastInDocumentGroup)) ? handleMouseLeave : undefined} >
= ({ className={buildClassName('message-content-wrapper', contentClassName.includes('text') && 'can-select-text')} >
= ({
{lang('ForwardedMessage')}
)} {renderContent()} - {(!isInDocumentGroup || isLastInDocumentGroup) && !(!webPage && !animatedEmoji && textParts) && ( - + {(!isInDocumentGroup || isLastInDocumentGroup) && !(!webPage && !hasAnimatedEmoji && textParts) && ( + <> + {hasReactionButtons && !areReactionsOutside && (hasAnimatedEmoji || !textParts || webPage) ? ( + + ) : renderMeta()} + )} {canShowActionButton && canForward ? (
{contextMenuPosition && ( = ({ ); }; -function handleDocumentGroupMouseEnter(e: React.MouseEvent) { - const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget); - if (lastGroupElement) { - lastGroupElement.setAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE, ''); - } -} - -function handleDocumentGroupMouseLeave(e: React.MouseEvent) { - const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget); - if (lastGroupElement) { - lastGroupElement.removeAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE); - } -} - -function getLastElementInDocumentGroup(element: Element) { - let current: Element | null = element; - - do { - current = current.nextElementSibling; - } while (current && !current.classList.contains('last-in-document-group')); - - return current; -} - export default memo(withGlobal( (global, ownProps): StateProps => { const { focusedMessage, forwardMessages, lastSyncTime } = global; const { - message, album, withSenderName, withAvatar, threadId, messageListType, + message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, } = ownProps; const { id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, threadInfo, @@ -884,6 +972,16 @@ export default memo(withGlobal( ? selectThreadInfo(global, threadInfo.chatId, threadInfo.threadId) || threadInfo : undefined; + const firstMessageInGroupId = message.groupedId + && selectMessageIdsByGroupId(global, chatId, message.groupedId)?.[0]; + const reactionsMessage = firstMessageInGroupId && !album + ? (isLastInDocumentGroup + ? selectChatMessage(global, chatId, firstMessageInGroupId) + : undefined) + : message; + + const localSticker = singleEmoji ? selectLocalAnimatedEmoji(global, singleEmoji) : undefined; + return { theme: selectTheme(global), chatUsername, @@ -898,6 +996,7 @@ export default memo(withGlobal( isProtected: selectIsMessageProtected(global, message), isFocused, isForwarding, + reactionsMessage, isChatWithSelf, isRepliesChat, isChannel, @@ -906,6 +1005,10 @@ export default memo(withGlobal( highlight, isSingleEmoji: Boolean(singleEmoji), animatedEmoji: singleEmoji ? selectAnimatedEmoji(global, singleEmoji) : undefined, + animatedEmojiEffect: singleEmoji && isUserId(chatId) ? selectAnimatedEmojiEffect(global, singleEmoji) : undefined, + animatedEmojiSoundId: singleEmoji ? selectAnimatedEmojiSound(global, singleEmoji) : undefined, + localSticker, + localEffect: localSticker && isUserId(chatId) ? selectLocalAnimatedEmojiEffect(localSticker) : undefined, isInSelectMode: selectIsInSelectMode(global), isSelected, isGroupSelected: ( @@ -922,6 +1025,10 @@ export default memo(withGlobal( ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(typeof uploadProgress === 'number' && { uploadProgress }), ...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }), + defaultReaction: selectDefaultReaction(global, chatId), + activeReaction: global.activeReactions[id], + activeEmojiInteraction: global.activeEmojiInteraction, + availableReactions: global.availableReactions, }; }, )(Message)); diff --git a/src/components/middle/message/MessageContextMenu.scss b/src/components/middle/message/MessageContextMenu.scss index 4125f6d23..415b86daa 100644 --- a/src/components/middle/message/MessageContextMenu.scss +++ b/src/components/middle/message/MessageContextMenu.scss @@ -2,11 +2,22 @@ position: absolute; font-size: 1rem; + .scrollable-content { + overflow: auto; + overflow: overlay; + padding: 0.5rem 0; + overscroll-behavior: contain; + } + .bubble { transform: scale(0.5); transition: opacity .15s cubic-bezier(0.2, 0, 0.2, 1), transform .15s cubic-bezier(0.2, 0, 0.2, 1) !important; - overflow: auto; - overflow: overlay; + overflow: initial; + padding: 0; + } + + &.with-reactions .bubble { + margin-top: 3.5rem; } .backdrop { diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index d39843907..c77e69222 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -2,31 +2,39 @@ import React, { FC, memo, useCallback, useEffect, useRef, } from '../../../lib/teact/teact'; -import { ApiMessage, ApiUser } from '../../../api/types'; +import { ApiAvailableReaction, ApiMessage, ApiUser } from '../../../api/types'; import { IAnchorPosition } from '../../../types'; import { getMessageCopyOptions } from './helpers/copyOptions'; import { disableScrolling, enableScrolling } from '../../../util/scrollLock'; import useContextMenuPosition from '../../../hooks/useContextMenuPosition'; import useLang from '../../../hooks/useLang'; +import buildClassName from '../../../util/buildClassName'; +import useFlag from '../../../hooks/useFlag'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; import Avatar from '../../common/Avatar'; +import ReactionSelector from './ReactionSelector'; import './MessageContextMenu.scss'; type OwnProps = { + availableReactions?: ApiAvailableReaction[]; isOpen: boolean; anchor: IAnchorPosition; message: ApiMessage; canSendNow?: boolean; + enabledReactions?: string[]; canReschedule?: boolean; canReply?: boolean; canPin?: boolean; canUnpin?: boolean; canDelete?: boolean; canReport?: boolean; + canShowReactionsCount?: boolean; + canShowReactionList?: boolean; + canRemoveReaction?: boolean; canEdit?: boolean; canForward?: boolean; canFaveSticker?: boolean; @@ -34,6 +42,7 @@ type OwnProps = { canCopy?: boolean; canCopyLink?: boolean; canSelect?: boolean; + isPrivate?: boolean; canDownload?: boolean; isDownloading?: boolean; canShowSeenBy?: boolean; @@ -55,13 +64,20 @@ type OwnProps = { onCopyLink?: () => void; onDownload?: () => void; onShowSeenBy?: () => void; + onShowReactors?: () => void; + onSendReaction: (reaction: string | undefined, x: number, y: number) => void; }; const SCROLLBAR_WIDTH = 10; +const REACTION_BUBBLE_EXTRA_WIDTH = 32; +const ANIMATION_DURATION = 200; const MessageContextMenu: FC = ({ + availableReactions, isOpen, message, + isPrivate, + enabledReactions, anchor, canSendNow, canReschedule, @@ -80,6 +96,9 @@ const MessageContextMenu: FC = ({ canDownload, isDownloading, canShowSeenBy, + canShowReactionsCount, + canRemoveReaction, + canShowReactionList, seenByRecentUsers, onReply, onEdit, @@ -98,10 +117,18 @@ const MessageContextMenu: FC = ({ onCopyLink, onDownload, onShowSeenBy, + onShowReactors, + onSendReaction, }) => { // eslint-disable-next-line no-null/no-null const menuRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const scrollableRef = useRef(null); const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined); + const noReactions = !isPrivate && !enabledReactions?.length; + const withReactions = canShowReactionList && !noReactions; + + const [isReady, markIsReady, unmarkIsReady] = useFlag(); const getTriggerElement = useCallback(() => { return document.querySelector(`.Transition__slide--active > .MessageList div[data-message-id="${message.id}"]`); @@ -117,6 +144,21 @@ const MessageContextMenu: FC = ({ [], ); + const handleRemoveReaction = useCallback(() => { + onSendReaction(undefined, 0, 0); + }, [onSendReaction]); + + useEffect(() => { + if (!isOpen) { + unmarkIsReady(); + return; + } + + setTimeout(() => { + markIsReady(); + }, ANIMATION_DURATION); + }, [isOpen, markIsReady, unmarkIsReady]); + const { positionX, positionY, style, menuStyle, withScroll, } = useContextMenuPosition( @@ -126,10 +168,11 @@ const MessageContextMenu: FC = ({ getMenuElement, SCROLLBAR_WIDTH, (document.querySelector('.MiddleHeader') as HTMLElement).offsetHeight, + withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined, ); useEffect(() => { - disableScrolling(withScroll ? menuRef.current : undefined); + disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector'); return enableScrolling; }, [withScroll]); @@ -144,51 +187,79 @@ const MessageContextMenu: FC = ({ positionY={positionY} style={style} menuStyle={menuStyle} - className="MessageContextMenu fluid" + className={buildClassName( + 'MessageContextMenu', 'fluid', withReactions && 'with-reactions', + )} onClose={onClose} onCloseAnimationEnd={onCloseAnimationEnd} > - {canSendNow && {lang('MessageScheduleSend')}} - {canReschedule && ( - {lang('MessageScheduleEditTime')} + {canShowReactionList && ( + )} - {canReply && {lang('Reply')}} - {canEdit && {lang('Edit')}} - {canFaveSticker && ( - {lang('AddToFavorites')} - )} - {canUnfaveSticker && ( - {lang('Stickers.RemoveFromFavorites')} - )} - {canCopy && copyOptions.map((options) => ( - {lang(options.label)} - ))} - {canPin && {lang('DialogPin')}} - {canUnpin && {lang('DialogUnpin')}} - {canDownload && ( - - {isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')} - - )} - {canForward && {lang('Forward')}} - {canSelect && {lang('Common.Select')}} - {canReport && {lang('lng_context_report_msg')}} - {canShowSeenBy && ( - - {message.seenByUserIds?.length - ? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i') - : lang('Conversation.ContextMenuNoViews')} -
- {seenByRecentUsers?.map((user) => ( - - ))} -
-
- )} - {canDelete && {lang('Delete')}} + +
+ {canRemoveReaction && Remove Reaction} + {canSendNow && {lang('MessageScheduleSend')}} + {canReschedule && ( + {lang('MessageScheduleEditTime')} + )} + {canReply && {lang('Reply')}} + {canEdit && {lang('Edit')}} + {canFaveSticker && ( + {lang('AddToFavorites')} + )} + {canUnfaveSticker && ( + {lang('Stickers.RemoveFromFavorites')} + )} + {canCopy && copyOptions.map((options) => ( + {lang(options.label)} + ))} + {canPin && {lang('DialogPin')}} + {canUnpin && {lang('DialogUnpin')}} + {canDownload && ( + + {isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')} + + )} + {canForward && {lang('Forward')}} + {canSelect && {lang('Common.Select')}} + {canReport && {lang('lng_context_report_msg')}} + {(canShowSeenBy || canShowReactionsCount) && ( + + {canShowReactionsCount && message.reactors?.count ? ( + canShowSeenBy && message.seenByUserIds?.length + ? lang('Chat.OutgoingContextMixedReactionCount', [message.reactors.count, message.seenByUserIds.length]) + : lang('Chat.ContextReactionCount', message.reactors.count, 'i')) + : (message.seenByUserIds?.length + ? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i') + : lang('Conversation.ContextMenuNoViews'))} +
+ {seenByRecentUsers?.map((user) => ( + + ))} +
+
+ )} + {canDelete && {lang('Delete')}} +
); }; diff --git a/src/components/middle/message/MessageMeta.scss b/src/components/middle/message/MessageMeta.scss index 11f37f572..e475da993 100644 --- a/src/components/middle/message/MessageMeta.scss +++ b/src/components/middle/message/MessageMeta.scss @@ -13,6 +13,12 @@ max-width: 100%; user-select: none; + .ReactionAnimatedEmoji { + width: 1rem; + height: 1rem; + margin-right: 0.25rem; + } + .message-time, .message-signature, .message-views { diff --git a/src/components/middle/message/MessageMeta.tsx b/src/components/middle/message/MessageMeta.tsx index 436a23e0d..c5315a990 100644 --- a/src/components/middle/message/MessageMeta.tsx +++ b/src/components/middle/message/MessageMeta.tsx @@ -2,31 +2,42 @@ import React, { FC, memo, useMemo, } from '../../../lib/teact/teact'; -import { ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types'; +import { ApiAvailableReaction, ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types'; +import { ActiveReaction } from '../../../global/types'; import { formatDateTimeToString, formatTime } from '../../../util/dateFormat'; import { formatIntegerCompact } from '../../../util/textFormat'; -import MessageOutgoingStatus from '../../common/MessageOutgoingStatus'; import renderText from '../../common/helpers/renderText'; import useLang from '../../../hooks/useLang'; import useFlag from '../../../hooks/useFlag'; +import buildClassName from '../../../util/buildClassName'; + +import MessageOutgoingStatus from '../../common/MessageOutgoingStatus'; +import ReactionAnimatedEmoji from './ReactionAnimatedEmoji'; import './MessageMeta.scss'; type OwnProps = { message: ApiMessage; + withReactions?: boolean; + withReactionOffset?: boolean; outgoingStatus?: ApiMessageOutgoingStatus; signature?: string; onClick: (e: React.MouseEvent) => void; + activeReaction?: ActiveReaction; + availableReactions?: ApiAvailableReaction[]; }; const MessageMeta: FC = ({ - message, outgoingStatus, signature, onClick, + message, outgoingStatus, signature, onClick, withReactions, + activeReaction, withReactionOffset, availableReactions, }) => { const lang = useLang(); const [isActivated, markActivated] = useFlag(); + const reactions = withReactions && message.reactions?.results.filter((l) => l.count > 0); + const title = useMemo(() => { if (!isActivated) return undefined; const createDateTime = formatDateTimeToString(message.date * 1000, lang.code); @@ -47,7 +58,19 @@ const MessageMeta: FC = ({ }, [isActivated, lang, message]); return ( - + + {reactions && reactions.map((l) => ( + + ))} {Boolean(message.views) && ( <> diff --git a/src/components/middle/message/ReactionAnimatedEmoji.scss b/src/components/middle/message/ReactionAnimatedEmoji.scss new file mode 100644 index 000000000..54ed223bf --- /dev/null +++ b/src/components/middle/message/ReactionAnimatedEmoji.scss @@ -0,0 +1,50 @@ +.ReactionAnimatedEmoji { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + &.is-animating { + // Fix for redundant scroll on iOS + transform: translateZ(0); + // Fix for redundant scroll in Firefox + contain: layout; + } + + .AnimatedSticker { + position: fixed; + top: -0.375rem; + left: -0.375rem; + pointer-events: none; + + &.effect { + top: -2.5rem; + left: -2.5rem; + } + + &:not(.open) { + opacity: 1 !important; + } + + &.closing { + opacity: 0 !important; + } + } + + &.in-meta { + .AnimatedSticker { + top: -0.4375rem; + left: -0.4375rem; + + &.effect { + top: -2.5625rem; + left: -2.5625rem; + } + + // Fix for weird positioning in Chrome + canvas { + position: absolute; + } + } + } +} diff --git a/src/components/middle/message/ReactionAnimatedEmoji.tsx b/src/components/middle/message/ReactionAnimatedEmoji.tsx new file mode 100644 index 000000000..a98518183 --- /dev/null +++ b/src/components/middle/message/ReactionAnimatedEmoji.tsx @@ -0,0 +1,92 @@ +import React, { FC, memo, useCallback } from '../../../lib/teact/teact'; +import { getDispatch } from '../../../lib/teact/teactn'; + +import { ActiveReaction } from '../../../global/types'; +import { ApiAvailableReaction, ApiMediaFormat } from '../../../api/types'; + +import buildClassName from '../../../util/buildClassName'; +import useMedia from '../../../hooks/useMedia'; +import useShowTransition from '../../../hooks/useShowTransition'; +import useFlag from '../../../hooks/useFlag'; + +import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; +import AnimatedSticker from '../../common/AnimatedSticker'; + +import './ReactionAnimatedEmoji.scss'; + +type OwnProps = { + reaction: string; + activeReaction?: ActiveReaction; + isInMeta?: boolean; + availableReactions?: ApiAvailableReaction[]; +}; + +const CENTER_ICON_SIZE = 30; +const EFFECT_SIZE = 100; + +const ReactionAnimatedEmoji: FC = ({ + reaction, + activeReaction, + isInMeta, + availableReactions, +}) => { + const { stopActiveReaction } = getDispatch(); + + const availableReaction = availableReactions?.find((r) => r.reaction === reaction); + const centerIconId = availableReaction?.centerIcon?.id; + const effectId = availableReaction?.aroundAnimation?.id; + const mediaDataCenterIcon = useMedia(`sticker${centerIconId}`, !centerIconId, ApiMediaFormat.Lottie); + const mediaDataEffect = useMedia(`sticker${effectId}`, !effectId, ApiMediaFormat.Lottie); + + const shouldPlay = Boolean(activeReaction?.reaction === reaction && mediaDataCenterIcon && mediaDataEffect); + const { + shouldRender: shouldRenderAnimation, + transitionClassNames: animationClassNames, + } = useShowTransition(shouldPlay, undefined, true, 'slow'); + + const handleEnded = useCallback(() => { + stopActiveReaction({ messageId: activeReaction?.messageId, reaction }); + }, [activeReaction?.messageId, reaction, stopActiveReaction]); + + const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag(); + const shouldRenderStatic = !shouldPlay || !isAnimationLoaded; + + const className = buildClassName( + 'ReactionAnimatedEmoji', + isInMeta && 'in-meta', + shouldRenderAnimation && 'is-animating', + ); + + return ( +
+ {shouldRenderStatic && } + {shouldRenderAnimation && ( + <> + + + + )} +
+ ); +}; + +export default memo(ReactionAnimatedEmoji); diff --git a/src/components/middle/message/ReactionButton.tsx b/src/components/middle/message/ReactionButton.tsx new file mode 100644 index 000000000..1fd369c82 --- /dev/null +++ b/src/components/middle/message/ReactionButton.tsx @@ -0,0 +1,79 @@ +import React, { + FC, memo, useCallback, useMemo, +} from '../../../lib/teact/teact'; +import { getDispatch, getGlobal } from '../../../lib/teact/teactn'; + +import { + ApiAvailableReaction, ApiMessage, ApiReactionCount, ApiUser, +} from '../../../api/types'; +import { ActiveReaction } from '../../../global/types'; + +import buildClassName from '../../../util/buildClassName'; +import { formatIntegerCompact } from '../../../util/textFormat'; + +import Button from '../../ui/Button'; +import Avatar from '../../common/Avatar'; +import ReactionAnimatedEmoji from './ReactionAnimatedEmoji'; + +import './Reactions.scss'; + +const MAX_REACTORS_AVATARS = 3; + +const ReactionButton: FC<{ + reaction: ApiReactionCount; + message: ApiMessage; + activeReaction?: ActiveReaction; + availableReactions?: ApiAvailableReaction[]; +}> = ({ + reaction, + message, + activeReaction, + availableReactions, +}) => { + const { sendReaction } = getDispatch(); + + const { recentReactions } = message.reactions!; + + const recentReactors = useMemo(() => { + if (!recentReactions || reaction.count > MAX_REACTORS_AVATARS) { + return undefined; + } + + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + + return recentReactions + .filter((recentReaction) => recentReaction.reaction === reaction.reaction) + .map((recentReaction) => usersById[recentReaction.userId]) + .filter(Boolean) as ApiUser[]; + }, [reaction, recentReactions]); + + const handleClick = useCallback(() => { + sendReaction({ + reaction: reaction.isChosen ? undefined : reaction.reaction, + chatId: message.chatId, + messageId: message.id, + }); + }, [message, reaction, sendReaction]); + + return ( + + ); +}; + +export default memo(ReactionButton); diff --git a/src/components/middle/message/ReactionSelector.scss b/src/components/middle/message/ReactionSelector.scss new file mode 100644 index 000000000..6438ad764 --- /dev/null +++ b/src/components/middle/message/ReactionSelector.scss @@ -0,0 +1,91 @@ +.ReactionSelector { + position: absolute; + height: 3rem; + background: var(--color-background); + min-width: 3rem; + max-width: calc(100% + 5rem); + z-index: 100; + border-radius: 3rem; + filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow)); + right: -3rem; + top: -3.5rem; + + .bubble-big { + position: absolute; + display: block; + content: ""; + right: 1.5rem; + bottom: -0.5rem; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: var(--color-background); + z-index: -1; + } + + .bubble-small { + position: absolute; + display: block; + content: ""; + right: 1.25rem; + bottom: -1.25rem; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--color-background); + } + + body.is-safari & { + filter: none; + box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow); + } + + body.is-safari .bubble-small, body.is-safari .bubble-big { + box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow); + } + + .items-wrapper { + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 3rem; + } + + .items { + padding: 0 1rem; + width: 100%; + height: 100%; + overflow-x: auto; + overflow: overlay; + overflow-y: hidden; + display: flex; + cursor: pointer; + align-items: center; + border-radius: 3rem; + } + + .reaction { + margin-left: 0.5rem; + position: relative; + min-width: 2rem; + min-height: 2rem; + + &:first-child { + margin-left: 0; + } + } + + .ReactionStaticEmoji { + width: 2rem; + position: absolute; + top: 0; + left: 0; + transform: scale(0.9); + } + + .AnimatedSticker { + position: absolute; + top: 0; + left: 0; + } +} diff --git a/src/components/middle/message/ReactionSelector.tsx b/src/components/middle/message/ReactionSelector.tsx new file mode 100644 index 000000000..1b9013eb3 --- /dev/null +++ b/src/components/middle/message/ReactionSelector.tsx @@ -0,0 +1,122 @@ +import React, { + FC, memo, useLayoutEffect, useRef, +} from '../../../lib/teact/teact'; + +import { ApiAvailableReaction, ApiMediaFormat } from '../../../api/types'; + +import useMedia from '../../../hooks/useMedia'; +import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; +import useFlag from '../../../hooks/useFlag'; +import { getTouchY } from '../../../util/scrollLock'; + +import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; +import AnimatedSticker from '../../common/AnimatedSticker'; + +import './ReactionSelector.scss'; + +const REACTION_SIZE = 32; + +type OwnProps = { + enabledReactions?: string[]; + onSendReaction: (reaction: string, x: number, y: number) => void; + isPrivate?: boolean; + availableReactions?: ApiAvailableReaction[]; + isReady?: boolean; +}; + +const AvailableReaction: FC<{ + reaction: ApiAvailableReaction; + isReady?: boolean; + onSendReaction: (reaction: string, x: number, y: number) => void; +}> = ({ reaction, onSendReaction, isReady }) => { + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); + const [isActivated, activate, deactivate] = useFlag(); + const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady, ApiMediaFormat.Lottie); + const [isAnimationLoaded, markAnimationLoaded] = useFlag(); + + function handleClick() { + if (!containerRef.current) return; + const { x, y } = containerRef.current.getBoundingClientRect(); + + onSendReaction(reaction.reaction, x, y); + } + + const shouldRenderPreview = !isAnimationLoaded; + const shouldRenderAnimated = mediaData; + const shouldPlay = isReady && isActivated; + + return ( +
+ {shouldRenderPreview && } + {shouldRenderAnimated && ( + + )} +
+ ); +}; +const ReactionSelector: FC = ({ + availableReactions, + enabledReactions, + onSendReaction, + isPrivate, + isReady, +}) => { + // eslint-disable-next-line no-null/no-null + const itemsScrollRef = useRef(null); + const [isHorizontalScrollEnabled, enableHorizontalScroll] = useFlag(false); + useHorizontalScroll(itemsScrollRef.current, !isHorizontalScrollEnabled); + + useLayoutEffect(() => { + enableHorizontalScroll(); + }, [enableHorizontalScroll]); + + const handleWheel = (e: React.WheelEvent | React.TouchEvent) => { + if (!itemsScrollRef) return; + const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e); + + if (deltaY) { + e.preventDefault(); + } + }; + + if ((!isPrivate && !enabledReactions?.length) || !availableReactions) return undefined; + + return ( +
+
+
+
+
+ {availableReactions?.map((reaction) => { + if (reaction.isInactive + || (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction)))) return undefined; + return ( + + ); + })} +
+
+
+ ); +}; + +export default memo(ReactionSelector); diff --git a/src/components/middle/message/Reactions.scss b/src/components/middle/message/Reactions.scss new file mode 100644 index 000000000..6e044ff13 --- /dev/null +++ b/src/components/middle/message/Reactions.scss @@ -0,0 +1,84 @@ +.Reactions { + display: flex; + flex-direction: row; + width: 100%; + flex-wrap: wrap; + margin-top: 0.25rem; + overflow: visible; + + .Button { + --reaction-background: var(--color-reaction); + display: flex; + flex-direction: row; + height: 1.75rem; + white-space: nowrap; + width: auto; + margin: 0.125rem; + padding: 0 0.5rem; + border: 2px solid transparent; + background-color: var(--reaction-background) !important; + border-radius: 1.75rem; + font-weight: 500; + font-variant-numeric: tabular-nums; + text-transform: none; + color: var(--accent-color); + overflow: visible; + + .ReactionAnimatedEmoji, .icon-reaction-filled { + width: 1.125rem; + height: 1.125rem; + margin-right: 0.25rem; + } + + .avatars { + display: flex; + + .Avatar { + margin: 0 0 0 -0.25rem; + border: 0.0625rem solid var(--reaction-background); + width: 1.25rem; + height: 1.25rem; + + &:first-child { + margin: 0; + } + } + } + + &.chosen { + border-color: var(--accent-color); + } + + &:hover { + --reaction-background: var(--hover-color-reaction); + } + + &:first-of-type { + margin-left: 0; + } + + &:last-of-type { + margin-right: 0; + } + } + + &.is-outside { + margin-top: 0.125rem; + } + + &.is-outside .Button { + --reaction-background: var(--pattern-color); + color: white; + .theme-dark & { + color: var(--accent-color); + } + + &.chosen { + border-color: white; + + .theme-dark & { + border-color: var(--accent-color); + } + } + } +} diff --git a/src/components/middle/message/Reactions.tsx b/src/components/middle/message/Reactions.tsx new file mode 100644 index 000000000..dfa476d41 --- /dev/null +++ b/src/components/middle/message/Reactions.tsx @@ -0,0 +1,43 @@ +import React, { FC, memo } from '../../../lib/teact/teact'; + +import { ApiAvailableReaction, ApiMessage } from '../../../api/types'; +import { ActiveReaction } from '../../../global/types'; + +import buildClassName from '../../../util/buildClassName'; + +import ReactionButton from './ReactionButton'; + +import './Reactions.scss'; + +type OwnProps = { + message: ApiMessage; + isOutside?: boolean; + activeReaction?: ActiveReaction; + availableReactions?: ApiAvailableReaction[]; + metaChildren?: React.ReactNode; +}; + +const Reactions: FC = ({ + message, + isOutside, + activeReaction, + availableReactions, + metaChildren, +}) => { + return ( +
+ {message.reactions!.results.map((reaction) => ( + + ))} + {metaChildren} +
+ ); +}; + +export default memo(Reactions); diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index 0e912a870..0ba33fece 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -104,6 +104,40 @@ } } + .MessageMeta.reactions-offset { + position: relative; + top: .375rem; + bottom: auto !important; + float: right; + line-height: 1; + height: calc(var(--message-meta-height, 1rem)); + margin-left: auto; + margin-right: -.5rem; + align-self: flex-end; + + .MessageOutgoingStatus .Transition { + max-height: calc(var(--message-meta-height, 1rem)); + overflow: hidden; + } + + html[data-message-text-size="12"] & { + top: .25rem; + } + + html[data-message-text-size="17"] & { + top: .4375rem; + } + + html[data-message-text-size="18"] &, + html[data-message-text-size="19"] & { + top: .5rem; + } + + html[data-message-text-size="20"] & { + top: .5625rem; + } + } + &.document:not(.text) { &::after { content: ""; diff --git a/src/components/middle/message/hooks/useOuterHandlers.ts b/src/components/middle/message/hooks/useOuterHandlers.ts index d99b194cc..cfb1fec06 100644 --- a/src/components/middle/message/hooks/useOuterHandlers.ts +++ b/src/components/middle/message/hooks/useOuterHandlers.ts @@ -1,5 +1,5 @@ import { RefObject } from 'react'; -import React, { useEffect } from '../../../../lib/teact/teact'; +import React, { useEffect, useRef } from '../../../../lib/teact/teact'; import { getDispatch } from '../../../../lib/teact/teactn'; import { IS_ANDROID, IS_TOUCH_ENV } from '../../../../util/environment'; @@ -8,9 +8,14 @@ import { captureEvents, SwipeDirection } from '../../../../util/captureEvents'; import useFlag from '../../../../hooks/useFlag'; import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur'; import stopEvent from '../../../../util/stopEvent'; +import { REM } from '../../../common/helpers/mediaDimensions'; const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350; const SWIPE_ANIMATION_DURATION = 150; +const QUICK_REACTION_DOUBLE_TAP_DELAY = 200; +const QUICK_REACTION_AREA_WIDTH = 3 * REM; +const QUICK_REACTION_AREA_HEIGHT = Number(REM); +const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover'; export default function useOuterHandlers( selectMessage: (e?: React.MouseEvent, groupedId?: string) => void, @@ -22,20 +27,53 @@ export default function useOuterHandlers( isProtected: boolean, onContextMenu: (e: React.MouseEvent) => void, handleBeforeContextMenu: (e: React.MouseEvent) => void, + chatId: string, + isContextMenuShown: boolean, + contentRef: RefObject, + isOwn: boolean, + shouldHandleMouseLeave: boolean, ) { - const { setReplyingToId } = getDispatch(); + const { setReplyingToId, sendDefaultReaction } = getDispatch(); + const [isQuickReactionVisible, markQuickReactionVisible, unmarkQuickReactionVisible] = useFlag(); const [isSwiped, markSwiped, unmarkSwiped] = useFlag(); + const doubleTapTimeoutRef = useRef(); function handleMouseDown(e: React.MouseEvent) { preventMessageInputBlur(e); handleBeforeContextMenu(e); } - function handleClick(e: React.MouseEvent) { - if (isInSelectMode) { - selectMessage(e); - } else if (IS_ANDROID) { + function handleMouseMove(e: React.MouseEvent) { + const container = contentRef.current; + if (!container) return; + + const { clientX, clientY } = e; + const { + x, width, y, height, + } = container.getBoundingClientRect(); + + const isVisibleX = Math.abs((isOwn ? (clientX - x) : (x + width - clientX))) < QUICK_REACTION_AREA_WIDTH; + const isVisibleY = Math.abs(y + height - clientY) < QUICK_REACTION_AREA_HEIGHT; + if (isVisibleX && isVisibleY) { + markQuickReactionVisible(); + } else { + unmarkQuickReactionVisible(); + } + } + + function handleSendQuickReaction(e: React.MouseEvent) { + const { x, y } = e.currentTarget.getBoundingClientRect(); + sendDefaultReaction({ + chatId, + messageId, + x, + y, + }); + } + + function handleTap(e: React.MouseEvent) { + if (IS_ANDROID) { const target = e.target as HTMLDivElement; if (!target.classList.contains('text-content') && !target.classList.contains('Message')) { return; @@ -51,6 +89,38 @@ export default function useOuterHandlers( } } + function handleDoubleTap(e: React.MouseEvent) { + const { pageX: x, pageY: y } = e; + + sendDefaultReaction({ + chatId, + messageId, + x, + y, + }); + } + + function handleClick(e: React.MouseEvent) { + if (isInSelectMode) { + selectMessage(e); + return; + } + + if (!IS_TOUCH_ENV) return; + + if (doubleTapTimeoutRef.current) { + clearInterval(doubleTapTimeoutRef.current); + doubleTapTimeoutRef.current = undefined; + handleDoubleTap(e); + return; + } + + doubleTapTimeoutRef.current = setTimeout(() => { + doubleTapTimeoutRef.current = undefined; + handleTap(e); + }, QUICK_REACTION_DOUBLE_TAP_DELAY); + } + function handleContextMenu(e: React.MouseEvent) { if (IS_ANDROID) { if ((e.target as HTMLElement).matches('a[href]')) { @@ -65,6 +135,8 @@ export default function useOuterHandlers( } function handleContainerDoubleClick() { + if (IS_TOUCH_ENV) return; + setReplyingToId({ messageId }); } @@ -73,7 +145,7 @@ export default function useOuterHandlers( } useEffect(() => { - if (!IS_TOUCH_ENV || isInSelectMode || !canReply) { + if (!IS_TOUCH_ENV || isInSelectMode || !canReply || isContextMenuShown) { return undefined; } @@ -104,7 +176,14 @@ export default function useOuterHandlers( startedAt = undefined; }, }); - }, [containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped, canReply]); + }, [ + containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped, canReply, isContextMenuShown, + ]); + + function handleMouseLeave(e: React.MouseEvent) { + unmarkQuickReactionVisible(); + if (shouldHandleMouseLeave) handleDocumentGroupMouseLeave(e); + } return { handleMouseDown: !isInSelectMode ? handleMouseDown : undefined, @@ -112,6 +191,35 @@ export default function useOuterHandlers( handleContextMenu: !isInSelectMode ? handleContextMenu : (isProtected ? stopEvent : undefined), handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined, handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined, + handleMouseMove, + handleSendQuickReaction, + handleMouseLeave, isSwiped, + isQuickReactionVisible, + handleDocumentGroupMouseEnter, }; } + +function handleDocumentGroupMouseEnter(e: React.MouseEvent) { + const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget); + if (lastGroupElement) { + lastGroupElement.setAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE, ''); + } +} + +function handleDocumentGroupMouseLeave(e: React.MouseEvent) { + const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget); + if (lastGroupElement) { + lastGroupElement.removeAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE); + } +} + +function getLastElementInDocumentGroup(element: Element) { + let current: Element | null = element; + + do { + current = current.nextElementSibling; + } while (current && !current.classList.contains('last-in-document-group')); + + return current; +} diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index 2160ed9d1..a55b0995b 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -123,6 +123,7 @@ const RightColumn: FC = ({ case ManagementScreens.ChatAdministrators: case ManagementScreens.ChannelSubscribers: case ManagementScreens.GroupMembers: + case ManagementScreens.Reactions: setManagementScreen(ManagementScreens.Initial); break; case ManagementScreens.GroupUserPermissionsCreate: diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 6adcd2af7..1c92468b6 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -17,10 +17,7 @@ import { selectUser, } from '../../modules/selectors'; import { - getCanAddContact, - isChatAdmin, - isChatChannel, - isUserId, + getCanAddContact, isChatAdmin, isChatChannel, isUserId, } from '../../modules/helpers'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useLang from '../../hooks/useLang'; @@ -84,6 +81,7 @@ enum HeaderContent { GifSearch, PollResults, AddingMembers, + ManageReactions, } const RightHeader: FC = ({ @@ -195,6 +193,8 @@ const RightHeader: FC = ({ HeaderContent.ManageGroupMembers ) : managementScreen === ManagementScreens.GroupAddAdmins ? ( HeaderContent.ManageGroupAddAdmins + ) : managementScreen === ManagementScreens.Reactions ? ( + HeaderContent.ManageReactions ) : undefined // Never reached ) : undefined; // When column is closed @@ -278,6 +278,8 @@ const RightHeader: FC = ({ case HeaderContent.MemberList: case HeaderContent.ManageGroupMembers: return

{lang('GroupMembers')}

; + case HeaderContent.ManageReactions: + return

{lang('Reactions')}

; default: return ( <> diff --git a/src/components/right/management/ManageChannel.tsx b/src/components/right/management/ManageChannel.tsx index a6246f952..792d9a1a6 100644 --- a/src/components/right/management/ManageChannel.tsx +++ b/src/components/right/management/ManageChannel.tsx @@ -36,6 +36,7 @@ type StateProps = { progress?: ManagementProgress; isSignaturesShown: boolean; canChangeInfo?: boolean; + availableReactionsCount?: number; }; const CHANNEL_TITLE_EMPTY = 'Channel title can\'t be empty'; @@ -46,6 +47,7 @@ const ManageChannel: FC = ({ progress, isSignaturesShown, canChangeInfo, + availableReactionsCount, onScreenSelect, onClose, isActive, @@ -92,6 +94,10 @@ const ManageChannel: FC = ({ onScreenSelect(ManagementScreens.Discussion); }, [onScreenSelect]); + const handleClickReactions = useCallback(() => { + onScreenSelect(ManagementScreens.Reactions); + }, [onScreenSelect]); + const handleClickAdministrators = useCallback(() => { onScreenSelect(ManagementScreens.ChatAdministrators); }, [onScreenSelect]); @@ -148,6 +154,8 @@ const ManageChannel: FC = ({ openChat({ id: undefined }); }, [chat.isCreator, chat.id, closeDeleteDialog, closeManagement, leaveChannel, deleteChannel, openChat]); + const enabledReactionsCount = chat.fullInfo?.enabledReactions?.length || 0; + if (chat.isRestricted) { return undefined; } @@ -202,6 +210,17 @@ const ManageChannel: FC = ({ {lang('ChannelAdministrators')} {adminsCount} + + {lang('Reactions')} + + {enabledReactionsCount}/{availableReactionsCount} + +
( progress, isSignaturesShown, canChangeInfo: getHasAdminRight(chat, 'changeInfo'), + availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length, }; }, )(ManageChannel)); diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx index acdbe9b8b..42d38ef09 100644 --- a/src/components/right/management/ManageGroup.tsx +++ b/src/components/right/management/ManageGroup.tsx @@ -40,6 +40,7 @@ type StateProps = { hasLinkedChannel: boolean; canChangeInfo?: boolean; canBanUsers?: boolean; + availableReactionsCount?: number; }; const GROUP_TITLE_EMPTY = 'Group title can\'t be empty'; @@ -59,6 +60,7 @@ const ManageGroup: FC = ({ onScreenSelect, onClose, isActive, + availableReactionsCount, }) => { const { togglePreHistoryHidden, @@ -100,6 +102,10 @@ const ManageGroup: FC = ({ onScreenSelect(ManagementScreens.Discussion); }, [onScreenSelect]); + const handleClickReactions = useCallback(() => { + onScreenSelect(ManagementScreens.Reactions); + }, [onScreenSelect]); + const handleClickPermissions = useCallback(() => { onScreenSelect(ManagementScreens.GroupPermissions); }, [onScreenSelect]); @@ -154,6 +160,8 @@ const ManageGroup: FC = ({ togglePreHistoryHidden({ chatId: chat.id, isEnabled: !isPreHistoryHidden }); }, [chat, togglePreHistoryHidden]); + const enabledReactionsCount = chat.fullInfo?.enabledReactions?.length || 0; + const enabledPermissionsCount = useMemo(() => { if (!chat.defaultBannedRights) { return 0; @@ -257,6 +265,18 @@ const ManageGroup: FC = ({ {enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT} + + + {lang('Reactions')} + + {enabledReactionsCount}/{availableReactionsCount} + + ( hasLinkedChannel, canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'), canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'), + availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length, }; }, )(ManageGroup)); diff --git a/src/components/right/management/ManageReactions.tsx b/src/components/right/management/ManageReactions.tsx new file mode 100644 index 000000000..2071c9cd2 --- /dev/null +++ b/src/components/right/management/ManageReactions.tsx @@ -0,0 +1,131 @@ +import React, { + FC, memo, useCallback, useEffect, useState, +} from '../../../lib/teact/teact'; +import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; + +import { ApiAvailableReaction, ApiChat } from '../../../api/types'; + +import { selectChat } from '../../../modules/selectors'; +import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; + +import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; +import Checkbox from '../../ui/Checkbox'; +import FloatingActionButton from '../../ui/FloatingActionButton'; +import Spinner from '../../ui/Spinner'; + +type OwnProps = { + chatId: string; + onClose: NoneToVoidFunction; + isActive: boolean; +}; + +type StateProps = { + chat?: ApiChat; + availableReactions?: ApiAvailableReaction[]; + enabledReactions?: string[]; +}; + +const ManageReactions: FC = ({ + availableReactions, + enabledReactions, + chat, + isActive, + onClose, +}) => { + const { setChatEnabledReactions } = getDispatch(); + + const lang = useLang(); + const [isTouched, setIsTouched] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [localEnabledReactions, setLocalEnabledReactions] = useState(enabledReactions); + + useHistoryBack(isActive, onClose); + + const handleSaveReactions = useCallback(() => { + if (!chat) return; + setIsLoading(true); + + setChatEnabledReactions({ + chatId: chat.id, + enabledReactions: localEnabledReactions, + }); + }, [chat, localEnabledReactions, setChatEnabledReactions]); + + useEffect(() => { + setIsLoading(false); + setIsTouched(false); + setLocalEnabledReactions(enabledReactions || []); + }, [enabledReactions]); + + const handleReactionChange = useCallback((e: React.ChangeEvent) => { + if (!chat || !availableReactions) return; + + const { name, checked } = e.currentTarget; + const newEnabledReactions = name === 'all' ? (checked ? availableReactions.map((l) => l.reaction) : []) + : (!checked + ? localEnabledReactions.filter((l) => l !== name) + : [...localEnabledReactions, name]); + + setLocalEnabledReactions(newEnabledReactions); + setIsTouched(true); + }, [availableReactions, chat, localEnabledReactions]); + + return ( +
+
+
+
+ 0} + label={lang('EnableReactions')} + onChange={handleReactionChange} + /> +
+ {availableReactions?.filter((l) => !l.isInactive).map(({ reaction, title }) => ( +
+ + + {title} +
+ )} + onChange={handleReactionChange} + /> +
+ ))} +
+
+ + + {isLoading ? ( + + ) : ( + + )} + +
+ ); +}; + +export default memo(withGlobal( + (global, { chatId }): StateProps => { + const chat = selectChat(global, chatId)!; + + return { + enabledReactions: chat.fullInfo?.enabledReactions, + availableReactions: global.availableReactions, + chat, + }; + }, +)(ManageReactions)); diff --git a/src/components/right/management/Management.scss b/src/components/right/management/Management.scss index f7d8703ca..5f9e2e68b 100644 --- a/src/components/right/management/Management.scss +++ b/src/components/right/management/Management.scss @@ -55,6 +55,16 @@ .ListItem { margin: 0 -.75rem; + .Reaction { + display: flex; + align-items: center; + } + + .ReactionStaticEmoji { + width: 1.5rem; + margin-right: 1rem; + } + &:last-child { margin-bottom: 0; } diff --git a/src/components/right/management/Management.tsx b/src/components/right/management/Management.tsx index 8ec9f1d5e..c59158fce 100644 --- a/src/components/right/management/Management.tsx +++ b/src/components/right/management/Management.tsx @@ -18,6 +18,7 @@ import ManageGroupRecentActions from './ManageGroupRecentActions'; import ManageGroupAdminRights from './ManageGroupAdminRights'; import ManageGroupMembers from './ManageGroupMembers'; import ManageGroupUserPermissionsCreate from './ManageGroupUserPermissionsCreate'; +import ManageReactions from './ManageReactions'; export type OwnProps = { chatId: string; @@ -239,6 +240,15 @@ const Management: FC = ({ onChatMemberSelect={onChatMemberSelect} /> ); + + case ManagementScreens.Reactions: + return ( + + ); } return undefined; // Never reached diff --git a/src/config.ts b/src/config.ts index 189241ac4..48dab80a2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,6 +58,7 @@ export const PROFILE_PHOTOS_LIMIT = 40; export const PROFILE_SENSITIVE_AREA = 500; export const COMMON_CHATS_LIMIT = 100; export const GROUP_CALL_PARTICIPANTS_LIMIT = 100; +export const REACTION_LIST_LIMIT = 100; export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20; export const ALL_CHATS_PRELOAD_DISABLED = false; diff --git a/src/global/cache.ts b/src/global/cache.ts index 375280f59..5520ff493 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -198,6 +198,10 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.messages.sponsoredByChatId) { cached.messages.sponsoredByChatId = {}; } + + if (!cached.activeReactions) { + cached.activeReactions = {}; + } } function updateCache() { @@ -243,6 +247,7 @@ function updateCache() { settings: reduceSettings(global), chatFolders: reduceChatFolders(global), groupCalls: reduceGroupCalls(global), + availableReactions: reduceAvailableReactions(global), }; const json = JSON.stringify(reducedGlobal); @@ -346,3 +351,8 @@ function reduceGroupCalls(global: GlobalState): GlobalState['groupCalls'] { isFallbackConfirmOpen: undefined, }; } + +function reduceAvailableReactions(global: GlobalState): GlobalState['availableReactions'] { + return global.availableReactions + ?.map((r) => pick(r, ['reaction', 'staticIcon', 'title', 'isInactive'])); +} diff --git a/src/global/initial.ts b/src/global/initial.ts index 55c325b62..f9fbb02cc 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -184,6 +184,7 @@ export const INITIAL_STATE: GlobalState = { }, twoFaSettings: {}, + activeReactions: {}, shouldShowContextMenuHint: true, diff --git a/src/global/types.ts b/src/global/types.ts index a738e9182..19c78e506 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -23,6 +23,8 @@ import { ApiCountryCode, ApiCountry, ApiGroupCall, + ApiAvailableReaction, + ApiAppConfig, ApiSponsoredMessage, } from '../api/types'; import { @@ -61,6 +63,23 @@ export interface MessageList { type: MessageListType; } +export interface ActiveEmojiInteraction { + x: number; + y: number; + messageId?: number; + endX?: number; + endY?: number; + startSize?: number; + reaction?: string; + animatedEffect?: string; + isReversed?: boolean; +} + +export interface ActiveReaction { + messageId?: number; + reaction?: string; +} + export interface Thread { listedIds?: number[]; outlyingIds?: number[]; @@ -86,6 +105,7 @@ export interface ServiceNotification { } export type GlobalState = { + appConfig?: ApiAppConfig; isChatInfoShown: boolean; isLeftColumnShown: boolean; isPollModalOpen?: boolean; @@ -212,6 +232,11 @@ export type GlobalState = { messageId: number; }; + reactorModal?: { + chatId: string; + messageId: number; + }; + fileUploads: { byMessageLocalId: Record>; gifs: { @@ -305,6 +331,10 @@ export type GlobalState = { globalUserIds?: string[]; }; + availableReactions?: ApiAvailableReaction[]; + activeEmojiInteraction?: ActiveEmojiInteraction; + activeReactions: Record; + localTextSearch: { byChatThreadKey: Record HTMLElement | null, extraPaddingX = 0, extraTopPadding = 0, + marginSides = 0, ) => { const [positionX, setPositionX] = useState<'right' | 'left'>('right'); const [positionY, setPositionY] = useState<'top' | 'bottom'>('bottom'); @@ -33,7 +34,14 @@ export default ( const rootEl = getRootElement(); const triggerRect = triggerEl.getBoundingClientRect(); - const menuRect = menuEl ? { width: menuEl.offsetWidth, height: menuEl.offsetHeight } : emptyRect; + + const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) : undefined; + + const menuRect = menuEl ? { + width: menuEl.offsetWidth, + height: menuEl.offsetHeight + marginTop!, + } : emptyRect; + const rootRect = rootEl ? rootEl.getBoundingClientRect() : emptyRect; let horizontalPostition: 'left' | 'right'; @@ -49,6 +57,19 @@ export default ( } setPositionX(horizontalPostition); + if (marginSides + && horizontalPostition === 'right' && (x + extraPaddingX + marginSides >= rootRect.width + rootRect.left)) { + x -= marginSides; + } + + if (marginSides && horizontalPostition === 'left') { + if (x + extraPaddingX + marginSides + menuRect.width >= rootRect.width + rootRect.left) { + x -= marginSides; + } else if (x - marginSides <= 0) { + x += marginSides; + } + } + if (y + menuRect.height < rootRect.height + rootRect.top) { setPositionY('top'); } else { @@ -63,17 +84,17 @@ export default ( ? Math.min(x - triggerRect.left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX) : Math.max((x - triggerRect.left), menuRect.width + MENU_POSITION_VISUAL_COMFORT_SPACE_PX); const top = Math.min( - rootRect.height - triggerRect.top + triggerRect.height - MENU_POSITION_BOTTOM_MARGIN, + rootRect.height - triggerRect.top + triggerRect.height - MENU_POSITION_BOTTOM_MARGIN + (marginTop || 0), y - triggerRect.top, ); - const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN; + const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0); setWithScroll(menuMaxHeight < menuRect.height); setMenuStyle(`max-height: ${menuMaxHeight}px;`); setStyle(`left: ${left}px; top: ${top}px`); }, [ anchor, extraPaddingX, extraTopPadding, - getMenuElement, getRootElement, getTriggerElement, + getMenuElement, getRootElement, getTriggerElement, marginSides, ]); return { diff --git a/src/hooks/useHorizontalScroll.ts b/src/hooks/useHorizontalScroll.ts index ef8333c0b..8013fb96d 100644 --- a/src/hooks/useHorizontalScroll.ts +++ b/src/hooks/useHorizontalScroll.ts @@ -2,7 +2,7 @@ import { useEffect } from '../lib/teact/teact'; export default (container: HTMLElement | null, isDisabled?: boolean) => { useEffect(() => { - if (!container) { + if (!container || isDisabled) { return undefined; } diff --git a/src/lib/gramjs/tl/AllTLObjects.js b/src/lib/gramjs/tl/AllTLObjects.js index 81a5f5255..6243e40a5 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 = 136; +const LAYER = 137; 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 06af53205..2a567798b 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -365,6 +365,7 @@ namespace Api { export type TypePeerSettings = messages.PeerSettings; export type TypeMessageReactionsList = messages.MessageReactionsList; export type TypeAvailableReactions = messages.AvailableReactionsNotModified | messages.AvailableReactions; + export type TypeTranslatedText = messages.TranslateNoResult | messages.TranslateResultText; } export namespace updates { @@ -7028,6 +7029,8 @@ namespace Api { selectAnimation: Api.TypeDocument; activateAnimation: Api.TypeDocument; effectAnimation: Api.TypeDocument; + aroundAnimation?: Api.TypeDocument; + centerIcon?: Api.TypeDocument; }> { // flags: undefined; inactive?: true; @@ -7038,6 +7041,8 @@ namespace Api { selectAnimation: Api.TypeDocument; activateAnimation: Api.TypeDocument; effectAnimation: Api.TypeDocument; + aroundAnimation?: Api.TypeDocument; + centerIcon?: Api.TypeDocument; }; export class ResPQ extends VirtualClass<{ nonce: int128; @@ -8089,6 +8094,12 @@ namespace Api { hash: int; reactions: Api.TypeAvailableReaction[]; }; + export class TranslateNoResult extends VirtualClass {}; + export class TranslateResultText extends VirtualClass<{ + text: string; + }> { + text: string; + }; } export namespace updates { @@ -11262,6 +11273,21 @@ namespace Api { }>, Bool> { reaction: string; }; + export class TranslateText extends Request, messages.TypeTranslatedText> { + // flags: undefined; + peer?: Api.TypeInputPeer; + msgId?: int; + text?: string; + fromLang?: string; + toLang: string; + }; } export namespace updates { @@ -12334,7 +12360,7 @@ namespace Api { | 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 | users.GetUsers | users.GetFullUser | users.SetSecureValueErrors | 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 - | 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.GetAllChats | 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.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.GetAllChats | 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 | updates.GetState | updates.GetDifference | updates.GetChannelDifference | photos.UpdateProfilePhoto | photos.UploadProfilePhoto | photos.DeletePhotos | photos.GetUserPhotos | upload.SaveFilePart | upload.GetFile | upload.SaveBigFilePart | upload.GetWebFile | upload.GetCdnFile | upload.ReuploadCdnFile | upload.GetCdnFileHashes | upload.GetFileHashes diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index b9377e8b5..3035db2af 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -947,9 +947,11 @@ reactionCount#6fb250d1 flags:# chosen:flags.0?true reaction:string count:int = R messageReactions#87b6e36 flags:# min:flags.0?true can_see_list:flags.2?true results:Vector recent_reactons:flags.1?Vector = MessageReactions; messageUserReaction#932844fa user_id:long reaction:string = MessageUserReaction; messages.messageReactionsList#a366923c flags:# count:int reactions:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; -availableReaction#21d7c4b flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document = AvailableReaction; +availableReaction#c077ec01 flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; +messages.translateNoResult#67ca4737 = messages.TranslatedText; +messages.translateResultText#a214f7d0 text:string = messages.TranslatedText; ---functions--- 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; invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X; @@ -1083,6 +1085,12 @@ messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector; messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; +messages.sendReaction#25690ce4 flags:# peer:InputPeer msg_id:int reaction:flags.0?string = Updates; +messages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector = Updates; +messages.getMessageReactionsList#e0ee6b77 flags:# peer:InputPeer id:int reaction:flags.0?string offset:flags.1?string limit:int = messages.MessageReactionsList; +messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector = Updates; +messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; +messages.setDefaultReaction#d960c4d4 reaction:string = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; @@ -1096,6 +1104,7 @@ help.getConfig#c4f9186b = Config; help.getNearestDc#1fb33026 = NearestDc; help.getSupport#9cdf08cd = help.Support; help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; +help.getAppConfig#98914110 = JSONValue; help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 8ad860617..692a458fc 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -190,5 +190,12 @@ "phone.exportGroupCallInvite", "phone.toggleGroupCallStartSubscription", "phone.joinGroupCallPresentation", - "phone.leaveGroupCallPresentation" + "phone.leaveGroupCallPresentation", + "messages.sendReaction", + "messages.getMessagesReactions", + "messages.getMessageReactionsList", + "messages.setChatAvailableReactions", + "messages.getAvailableReactions", + "messages.setDefaultReaction", + "help.getAppConfig" ] diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index 7f1f7f43e..b98f3919b 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -1313,11 +1313,14 @@ messageUserReaction#932844fa user_id:long reaction:string = MessageUserReaction; messages.messageReactionsList#a366923c flags:# count:int reactions:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; -availableReaction#21d7c4b flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document = AvailableReaction; +availableReaction#c077ec01 flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; +messages.translateNoResult#67ca4737 = messages.TranslatedText; +messages.translateResultText#a214f7d0 text:string = messages.TranslatedText; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1602,6 +1605,7 @@ messages.getMessageReactionsList#e0ee6b77 flags:# peer:InputPeer id:int reaction messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector = Updates; messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#d960c4d4 reaction:string = Bool; +messages.translateText#24ce6dee flags:# peer:flags.0?InputPeer msg_id:flags.0?int text:flags.1?string from_lang:flags.2?string to_lang:string = messages.TranslatedText; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1750,4 +1754,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 136 +// LAYER 137 diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index c264ca7cd..a3b23071c 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -90,6 +90,7 @@ class RLottie { private params: Params = {}, private onLoad?: () => void, private customColor?: [number, number, number], + private onEnded?: (isDestroyed?: boolean) => void, ) { this.initContainer(); this.initConfig(); @@ -359,6 +360,7 @@ class RLottie { if (delta > 0 && (frameIndex === this.framesCount! - 1 || expectedNextFrameIndex > this.framesCount! - 1)) { if (this.params.noLoop) { this.isAnimating = false; + this.onEnded?.(); return false; } @@ -368,6 +370,7 @@ class RLottie { } else if (delta < 0 && (frameIndex === 0 || expectedNextFrameIndex < 0)) { if (this.params.noLoop) { this.isAnimating = false; + this.onEnded?.(); return false; } diff --git a/src/modules/actions/all.ts b/src/modules/actions/all.ts index 4b2f03d5e..f8e2ae8ac 100644 --- a/src/modules/actions/all.ts +++ b/src/modules/actions/all.ts @@ -23,6 +23,7 @@ import './api/bots'; import './api/settings'; import './api/twoFaSettings'; import './api/payments'; +import './api/reactions'; import './apiUpdaters/initial'; import './apiUpdaters/chats'; diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 377ba84d5..6910bb098 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -1004,6 +1004,22 @@ addReducer('toggleIsProtected', (global, actions, payload) => { void callApi('toggleIsProtected', { chat, isProtected }); }); +addReducer('setChatEnabledReactions', (global, actions, payload) => { + const { chatId, enabledReactions } = payload; + const chat = selectChat(global, chatId); + + if (!chat) return; + + (async () => { + await callApi('setChatEnabledReactions', { + chat, + enabledReactions, + }); + + await loadFullChat(chat); + })(); +}); + async function loadChats(listType: 'active' | 'archived', offsetId?: string, offsetDate?: number) { let global = getGlobal(); diff --git a/src/modules/actions/api/messages.ts b/src/modules/actions/api/messages.ts index 8f9a1208b..f8934c978 100644 --- a/src/modules/actions/api/messages.ts +++ b/src/modules/actions/api/messages.ts @@ -16,7 +16,11 @@ import { } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; -import { MAX_MEDIA_FILES_FOR_ALBUM, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; +import { + MAX_MEDIA_FILES_FOR_ALBUM, + MESSAGE_LIST_SLICE, + SERVICE_NOTIFICATIONS_USER_ID, +} from '../../../config'; import { IS_IOS } from '../../../util/environment'; import { callApi, cancelApiProgress } from '../../../api/gramjs'; import { diff --git a/src/modules/actions/api/reactions.ts b/src/modules/actions/api/reactions.ts new file mode 100644 index 000000000..02f7c5566 --- /dev/null +++ b/src/modules/actions/api/reactions.ts @@ -0,0 +1,274 @@ +import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; +import { callApi } from '../../../api/gramjs'; +import * as mediaLoader from '../../../util/mediaLoader'; +import { ApiAppConfig, ApiMediaFormat } from '../../../api/types'; +import { + selectChat, + selectChatMessage, + selectDefaultReaction, + selectLocalAnimatedEmojiEffectByName, +} from '../../selectors'; +import { addMessageReaction, subtractXForEmojiInteraction } from '../../reducers/reactions'; +import { addUsers, updateChatMessage } from '../../reducers'; +import { buildCollectionByKey, omit } from '../../../util/iteratees'; +import { ANIMATION_LEVEL_MAX } from '../../../config'; + +addReducer('loadAvailableReactions', () => { + (async () => { + const result = await callApi('getAvailableReactions'); + + if (!result) { + return; + } + + // Preload animations + result.forEach((availableReaction) => { + if (availableReaction.aroundAnimation) { + mediaLoader.fetch(`sticker${availableReaction.aroundAnimation.id}`, ApiMediaFormat.Lottie); + } + if (availableReaction.centerIcon) { + mediaLoader.fetch(`sticker${availableReaction.centerIcon.id}`, ApiMediaFormat.Lottie); + } + }); + + setGlobal({ + ...getGlobal(), + availableReactions: result, + }); + })(); +}); + +addReducer('interactWithAnimatedEmoji', (global, actions, payload) => { + const { + emoji, x, y, localEffect, startSize, isReversed, + } = payload!; + + return { + ...global, + activeEmojiInteraction: { + animatedEffect: emoji || localEffect, + x: subtractXForEmojiInteraction(global, x), + y, + startSize, + isReversed, + }, + }; +}); + +addReducer('sendEmojiInteraction', (global, actions, payload) => { + const { + messageId, chatId, emoji, interactions, localEffect, + x, y, startX, startY, startSize, + } = payload!; + + const chat = selectChat(global, chatId); + + if (!chat || (!emoji && !localEffect)) { + return undefined; + } + + void callApi('sendEmojiInteraction', { + chat, + messageId, + emoticon: emoji || selectLocalAnimatedEmojiEffectByName(localEffect), + timestamps: interactions, + }); + + if (!global.activeEmojiInteraction) return undefined; + + return { + ...global, + activeEmojiInteraction: { + ...global.activeEmojiInteraction, + endX: subtractXForEmojiInteraction(global, x), + endY: y, + ...(startX && { x: subtractXForEmojiInteraction(global, startX) }), + ...(startY && { y: startY }), + ...(startSize && { startSize }), + }, + }; +}); + +addReducer('sendDefaultReaction', (global, actions, payload) => { + const { + chatId, messageId, x, y, + } = payload; + const reaction = selectDefaultReaction(global, chatId); + + if (!reaction) return; + + actions.sendReaction({ + chatId, + messageId, + reaction, + x, + y, + }); +}); + +addReducer('sendReaction', (global, actions, payload) => { + const { + chatId, messageId, + }: { messageId: number; chatId: string } = payload; + + let { reaction } = payload; + + const chat = selectChat(global, chatId); + const message = selectChatMessage(global, chatId, messageId); + + if (!chat || !message) { + return undefined; + } + + if (message.reactions?.results?.some((l) => l.reaction === reaction && l.isChosen)) { + reaction = undefined; + } + + void callApi('sendReaction', { chat, messageId, reaction }); + + const { animationLevel } = global.settings.byKey; + + if (animationLevel === ANIMATION_LEVEL_MAX) { + global = { + ...global, + activeReactions: { + ...(reaction ? global.activeReactions : omit(global.activeReactions, [messageId])), + ...(reaction && { + [messageId]: { + reaction, + messageId, + }, + }), + }, + }; + } + + return addMessageReaction(global, chatId, messageId, reaction); +}); + +addReducer('openChat', (global) => { + return { + ...global, + activeReactions: {}, + }; +}); + +addReducer('stopActiveReaction', (global, actions, payload) => { + const { messageId, reaction } = payload; + + if (global.activeReactions[messageId]?.reaction !== reaction) { + return global; + } + + return { + ...global, + activeReactions: omit(global.activeReactions, [messageId]), + }; +}); + +addReducer('setDefaultReaction', (global, actions, payload) => { + const { reaction } = payload; + + (async () => { + const result = await callApi('setDefaultReaction', { reaction }); + + if (!result) { + return; + } + + global = getGlobal(); + setGlobal({ + ...global, + appConfig: { + ...global.appConfig, + defaultReaction: reaction, + } as ApiAppConfig, + }); + })(); +}); + +addReducer('stopActiveEmojiInteraction', (global) => { + return { + ...global, + activeEmojiInteraction: undefined, + }; +}); + +addReducer('loadReactors', (global, actions, payload) => { + const { chatId, messageId, reaction } = payload; + const chat = selectChat(global, chatId); + const message = selectChatMessage(global, chatId, messageId); + if (!chat || !message) { + return; + } + + const offset = message.reactors?.nextOffset; + + (async () => { + const result = await callApi('fetchMessageReactionsList', { + reaction, + chat, + messageId, + offset, + }); + + if (!result) { + return; + } + + global = getGlobal(); + if (result.users?.length) { + global = addUsers(global, buildCollectionByKey(result.users, 'id')); + } + + const { nextOffset, count, reactions } = result; + + setGlobal(updateChatMessage(global, chatId, messageId, { + reactors: { + nextOffset, + count, + reactions: [ + ...(message.reactors?.reactions || []), + ...reactions, + ], + }, + })); + })(); +}); + +addReducer('loadMessageReactions', (global, actions, payload) => { + const { ids, chatId } = payload; + + const chat = selectChat(global, chatId); + + if (!chat) { + return; + } + + callApi('fetchMessageReactions', { ids, chat }); +}); + +addReducer('sendWatchingEmojiInteraction', (global, actions, payload) => { + const { + chatId, emoticon, x, y, startSize, isReversed, + } = payload; + + const chat = selectChat(global, chatId); + + if (!chat || !global.activeEmojiInteraction) { + return undefined; + } + + callApi('sendWatchingEmojiInteraction', { chat, emoticon }); + + return { + ...global, + activeEmojiInteraction: { + ...global.activeEmojiInteraction, + x: subtractXForEmojiInteraction(global, x), + y, + startSize, + isReversed, + }, + }; +}); diff --git a/src/modules/actions/api/settings.ts b/src/modules/actions/api/settings.ts index 3c44cc758..fce7628a3 100644 --- a/src/modules/actions/api/settings.ts +++ b/src/modules/actions/api/settings.ts @@ -602,3 +602,16 @@ addReducer('ensureTimeFormat', (global, actions) => { } })(); }); + +addReducer('loadAppConfig', () => { + (async () => { + const appConfig = await callApi('fetchAppConfig'); + + if (!appConfig) return; + + setGlobal({ + ...getGlobal(), + appConfig, + }); + })(); +}); diff --git a/src/modules/actions/api/symbols.ts b/src/modules/actions/api/symbols.ts index 66e0e27ef..41661fa45 100644 --- a/src/modules/actions/api/symbols.ts +++ b/src/modules/actions/api/symbols.ts @@ -100,6 +100,7 @@ addReducer('loadStickers', (global, actions, payload) => { addReducer('loadAnimatedEmojis', () => { void loadAnimatedEmojis(); + void loadAnimatedEmojiEffects(); }); addReducer('loadSavedGifs', (global) => { @@ -291,6 +292,20 @@ async function loadAnimatedEmojis() { setGlobal(replaceAnimatedEmojis(getGlobal(), { ...set, stickers })); } +async function loadAnimatedEmojiEffects() { + const stickerSet = await callApi('fetchAnimatedEmojiEffects'); + if (!stickerSet) { + return; + } + + const { set, stickers } = stickerSet; + + setGlobal({ + ...getGlobal(), + animatedEmojiEffects: { ...set, stickers }, + }); +} + function unfaveSticker(sticker: ApiSticker) { const global = getGlobal(); diff --git a/src/modules/actions/apiUpdaters/messages.ts b/src/modules/actions/apiUpdaters/messages.ts index 0bfc834ef..76c23e704 100644 --- a/src/modules/actions/apiUpdaters/messages.ts +++ b/src/modules/actions/apiUpdaters/messages.ts @@ -17,7 +17,7 @@ import { deleteChatScheduledMessages, updateThreadUnreadFromForwardedMessage, } from '../../reducers'; -import { GlobalActions, GlobalState } from '../../../global/types'; +import { ActiveEmojiInteraction, GlobalActions, GlobalState } from '../../../global/types'; import { selectChatMessage, selectChatMessages, @@ -39,6 +39,8 @@ import { selectChat, selectIsChatWithBot, selectIsServiceChatReady, + selectLocalAnimatedEmojiEffect, + selectLocalAnimatedEmoji, } from '../../selectors'; import { getMessageContent, isUserId, isMessageLocal } from '../../helpers'; @@ -108,6 +110,26 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { break; } + case 'updateStartEmojiInteraction': { + const { chatId: currentChatId } = selectCurrentMessageList(global) || {}; + + if (global.activeEmojiInteraction || currentChatId !== update.id) return; + + const localEmoji = selectLocalAnimatedEmoji(global, update.emoji); + + global = { + ...global, + activeEmojiInteraction: { + animatedEffect: localEmoji ? selectLocalAnimatedEmojiEffect(localEmoji) : update.emoji, + messageId: update.messageId, + } as ActiveEmojiInteraction, + }; + + setGlobal(global); + + break; + } + case 'newScheduledMessage': { const { chatId, id, message } = update; @@ -444,6 +466,18 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { break; } + + case 'updateMessageReactions': { + setGlobal(updateChatMessage( + global, + update.chatId, + update.id, + { + reactions: update.reactions, + }, + )); + break; + } } }); diff --git a/src/modules/actions/ui/messages.ts b/src/modules/actions/ui/messages.ts index 287d5c08f..8a80bed78 100644 --- a/src/modules/actions/ui/messages.ts +++ b/src/modules/actions/ui/messages.ts @@ -642,6 +642,22 @@ addReducer('createServiceNotification', (global, actions, payload) => { }); }); +addReducer('openReactorListModal', (global, actions, payload) => { + const { chatId, messageId } = payload!; + + return { + ...global, + reactorModal: { chatId, messageId }, + }; +}); + +addReducer('closeReactorListModal', (global) => { + return { + ...global, + reactorModal: undefined, + }; +}); + addReducer('openSeenByModal', (global, actions, payload) => { const { chatId, messageId } = payload!; diff --git a/src/modules/helpers/messages.ts b/src/modules/helpers/messages.ts index 76c31d20d..fe95ec89f 100644 --- a/src/modules/helpers/messages.ts +++ b/src/modules/helpers/messages.ts @@ -1,5 +1,5 @@ import { - ApiChat, ApiMessage, ApiMessageEntityTypes, ApiUser, + ApiChat, ApiMessage, ApiMessageEntityTypes, ApiReactions, ApiUser, } from '../../api/types'; import { LangFn } from '../../hooks/useLang'; @@ -271,3 +271,7 @@ export function getMessageContentFilename(message: ApiMessage) { return baseFilename; } + +export function areReactionsEmpty(reactions: ApiReactions) { + return !reactions.results.some((l) => l.count > 0); +} diff --git a/src/modules/reducers/reactions.ts b/src/modules/reducers/reactions.ts new file mode 100644 index 000000000..620d529aa --- /dev/null +++ b/src/modules/reducers/reactions.ts @@ -0,0 +1,82 @@ +import { updateChatMessage } from './messages'; +import { GlobalState } from '../../global/types'; +import { selectChatMessage } from '../selectors'; +import { MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config'; +import { + MIN_LEFT_COLUMN_WIDTH, + SIDE_COLUMN_MAX_WIDTH, +} from '../../components/middle/helpers/calculateMiddleFooterTransforms'; +import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; +import windowSize from '../../util/windowSize'; + +function getLeftColumnWidth(windowWidth: number) { + if (windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN) { + return Math.min( + Math.max(windowWidth * 0.25, MIN_LEFT_COLUMN_WIDTH), + windowWidth * 0.33, + ); + } + + if (windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN) { + return Math.min( + Math.max(windowWidth * 0.33, MIN_LEFT_COLUMN_WIDTH), + windowWidth * 0.4, + ); + } + + return SIDE_COLUMN_MAX_WIDTH; +} + +export function subtractXForEmojiInteraction(global: GlobalState, x: number) { + return x - ((global.isLeftColumnShown && !IS_SINGLE_COLUMN_LAYOUT) + ? global.leftColumnWidth || getLeftColumnWidth(windowSize.get().width) + : 0); +} + +export function addMessageReaction(global: GlobalState, chatId: string, messageId: number, reaction: string) { + const { reactions } = selectChatMessage(global, chatId, messageId) || {}; + + if (!reactions) { + return global; + } + + // Update UI without waiting for server response + let results = reactions.results.map((l) => (l.reaction === reaction + ? { + ...l, + count: l.isChosen ? l.count : l.count + 1, + isChosen: true, + } : (l.isChosen ? { + ...l, + isChosen: false, + count: l.count - 1, + } : l))) + .filter((l) => l.count > 0); + + let { recentReactions } = reactions; + + if (reaction && !results.some((l) => l.reaction === reaction)) { + const { currentUserId } = global; + + results = [...results, { + reaction, + isChosen: true, + count: 1, + }]; + + if (reactions.canSeeList) { + recentReactions = [...(recentReactions || []), { + userId: currentUserId!, + reaction, + }]; + } + } + + return updateChatMessage(global, chatId, messageId, { + reactions: { + ...reactions, + results, + recentReactions, + }, + }); +} diff --git a/src/modules/selectors/messages.ts b/src/modules/selectors/messages.ts index 674e52775..302586e40 100644 --- a/src/modules/selectors/messages.ts +++ b/src/modules/selectors/messages.ts @@ -880,3 +880,27 @@ export function selectSponsoredMessage(global: GlobalState, chatId: string) { return message && message.expiresAt >= Math.round(Date.now() / 1000) ? message : undefined; } + +export function selectDefaultReaction(global: GlobalState, chatId: string) { + if (chatId === SERVICE_NOTIFICATIONS_USER_ID) return undefined; + + const isPrivate = isUserId(chatId); + const defaultReaction = global.appConfig?.defaultReaction; + const { availableReactions } = global; + if (!defaultReaction || !availableReactions?.some( + (l) => l.reaction === defaultReaction && !l.isInactive, + )) { + return undefined; + } + + if (isPrivate) { + return defaultReaction; + } + + const enabledReactions = selectChat(global, chatId)?.fullInfo?.enabledReactions; + if (!enabledReactions?.includes(defaultReaction)) { + return undefined; + } + + return defaultReaction; +} diff --git a/src/modules/selectors/symbols.ts b/src/modules/selectors/symbols.ts index 4d75e690d..c1ae4f681 100644 --- a/src/modules/selectors/symbols.ts +++ b/src/modules/selectors/symbols.ts @@ -43,14 +43,47 @@ export function selectStickersForEmoji(global: GlobalState, emoji: string) { return stickersForEmoji; } +function cleanEmoji(emoji: string) { + // Some emojis (❤️ for example) with a service symbol 'VARIATION SELECTOR-16' are not recognized as animated + return emoji.replace('\ufe0f', ''); +} + export function selectAnimatedEmoji(global: GlobalState, emoji: string) { const { animatedEmojis } = global; if (!animatedEmojis || !animatedEmojis.stickers) { return undefined; } - // Some emojis (❤️ for example) with a service symbol 'VARIATION SELECTOR-16' are not recognized as animated - const cleanedEmoji = emoji.replace('\ufe0f', ''); + const cleanedEmoji = cleanEmoji(emoji); return animatedEmojis.stickers.find((sticker) => sticker.emoji === emoji || sticker.emoji === cleanedEmoji); } + +export function selectAnimatedEmojiEffect(global: GlobalState, emoji: string) { + const { animatedEmojiEffects } = global; + if (!animatedEmojiEffects || !animatedEmojiEffects.stickers) { + return undefined; + } + + const cleanedEmoji = cleanEmoji(emoji); + + return animatedEmojiEffects.stickers.find((sticker) => sticker.emoji === emoji || sticker.emoji === cleanedEmoji); +} + +export function selectAnimatedEmojiSound(global: GlobalState, emoji: string) { + return global?.appConfig?.emojiSounds[cleanEmoji(emoji)]; +} + +export function selectLocalAnimatedEmoji(global: GlobalState, emoji: string) { + const cleanedEmoji = cleanEmoji(emoji); + + return cleanedEmoji === '🍑' ? 'Peach' : (cleanedEmoji === '🍆' ? 'Eggplant' : undefined); +} + +export function selectLocalAnimatedEmojiEffect(emoji: string) { + return emoji === 'Eggplant' ? 'Cumshot' : undefined; +} + +export function selectLocalAnimatedEmojiEffectByName(name: string) { + return name === 'Cumshot' ? '🍆' : undefined; +} diff --git a/src/styles/Telegram T.json b/src/styles/Telegram T.json index e8ffbd808..e18f0102d 100644 --- a/src/styles/Telegram T.json +++ b/src/styles/Telegram T.json @@ -2,7 +2,7 @@ "metadata": { "name": "Telegram T", "lastOpened": 0, - "created": 1637672665426 + "created": 1639657599258 }, "iconSets": [ { @@ -157,13 +157,29 @@ }, { "selection": [ + { + "order": 698, + "id": 48, + "name": "reaction-filled", + "prevSize": 32, + "code": 59796, + "tempChar": "" + }, + { + "order": 695, + "id": 47, + "name": "reactions", + "prevSize": 32, + "code": 59795, + "tempChar": "" + }, { "order": 693, "id": 46, "name": "sidebar", "prevSize": 32, "code": 59794, - "tempChar": "" + "tempChar": "" }, { "order": 690, @@ -171,7 +187,7 @@ "name": "video-stop", "prevSize": 32, "code": 59787, - "tempChar": "" + "tempChar": "" }, { "order": 678, @@ -179,7 +195,7 @@ "name": "speaker", "prevSize": 32, "code": 59777, - "tempChar": "" + "tempChar": "" }, { "order": 679, @@ -187,7 +203,7 @@ "name": "speaker-outline", "prevSize": 32, "code": 59778, - "tempChar": "" + "tempChar": "" }, { "order": 680, @@ -195,7 +211,7 @@ "name": "phone-discard-outline", "prevSize": 32, "code": 59779, - "tempChar": "" + "tempChar": "" }, { "order": 681, @@ -203,7 +219,7 @@ "name": "allow-speak", "prevSize": 32, "code": 59780, - "tempChar": "" + "tempChar": "" }, { "order": 682, @@ -211,7 +227,7 @@ "name": "stop-raising-hand", "prevSize": 32, "code": 59781, - "tempChar": "" + "tempChar": "" }, { "order": 683, @@ -219,7 +235,7 @@ "name": "share-screen", "prevSize": 32, "code": 59782, - "tempChar": "" + "tempChar": "" }, { "order": 684, @@ -227,7 +243,7 @@ "name": "voice-chat", "prevSize": 32, "code": 59783, - "tempChar": "" + "tempChar": "" }, { "order": 689, @@ -235,7 +251,7 @@ "name": "video", "prevSize": 32, "code": 59784, - "tempChar": "" + "tempChar": "" }, { "order": 686, @@ -243,7 +259,7 @@ "name": "noise-suppression", "prevSize": 32, "code": 59785, - "tempChar": "" + "tempChar": "" }, { "order": 688, @@ -251,7 +267,7 @@ "name": "phone-discard", "prevSize": 32, "code": 59786, - "tempChar": "" + "tempChar": "" }, { "order": 667, @@ -259,7 +275,7 @@ "name": "bot-commands-filled", "prevSize": 32, "code": 59775, - "tempChar": "" + "tempChar": "" }, { "order": 664, @@ -267,7 +283,7 @@ "name": "reply-filled", "prevSize": 32, "code": 59776, - "tempChar": "" + "tempChar": "" }, { "order": 656, @@ -275,7 +291,7 @@ "name": "bug", "prevSize": 32, "code": 59774, - "tempChar": "" + "tempChar": "" }, { "order": 619, @@ -283,7 +299,7 @@ "name": "data", "prevSize": 32, "code": 59773, - "tempChar": "" + "tempChar": "" }, { "order": 622, @@ -291,7 +307,7 @@ "name": "darkmode", "prevSize": 32, "code": 59769, - "tempChar": "" + "tempChar": "" }, { "order": 623, @@ -299,7 +315,7 @@ "name": "animations", "prevSize": 32, "code": 59770, - "tempChar": "" + "tempChar": "" }, { "order": 626, @@ -307,7 +323,7 @@ "name": "enter", "prevSize": 32, "code": 59771, - "tempChar": "" + "tempChar": "" }, { "order": 627, @@ -315,7 +331,7 @@ "name": "fontsize", "prevSize": 32, "code": 59772, - "tempChar": "" + "tempChar": "" }, { "order": 630, @@ -323,7 +339,7 @@ "name": "permissions", "prevSize": 32, "code": 59766, - "tempChar": "" + "tempChar": "" }, { "order": 631, @@ -331,7 +347,7 @@ "name": "card", "prevSize": 32, "code": 59767, - "tempChar": "" + "tempChar": "" }, { "order": 634, @@ -339,7 +355,7 @@ "name": "truck", "prevSize": 32, "code": 59768, - "tempChar": "" + "tempChar": "" }, { "order": 663, @@ -347,7 +363,7 @@ "name": "share-filled", "prevSize": 32, "code": 59738, - "tempChar": "" + "tempChar": "" }, { "order": 638, @@ -355,7 +371,7 @@ "name": "bold", "prevSize": 32, "code": 59745, - "tempChar": "" + "tempChar": "" }, { "order": 639, @@ -363,7 +379,7 @@ "name": "bot-command", "prevSize": 32, "code": 59746, - "tempChar": "" + "tempChar": "" }, { "order": 642, @@ -371,7 +387,7 @@ "name": "calendar-filter", "prevSize": 32, "code": 59747, - "tempChar": "" + "tempChar": "" }, { "order": 643, @@ -379,7 +395,7 @@ "name": "comments", "prevSize": 32, "code": 59748, - "tempChar": "" + "tempChar": "" }, { "order": 645, @@ -387,7 +403,7 @@ "name": "comments-sticker", "prevSize": 32, "code": 59749, - "tempChar": "" + "tempChar": "" }, { "order": 646, @@ -395,7 +411,7 @@ "name": "arrow-down", "prevSize": 32, "code": 59750, - "tempChar": "" + "tempChar": "" }, { "order": 668, @@ -403,7 +419,7 @@ "name": "email", "prevSize": 32, "code": 59751, - "tempChar": "" + "tempChar": "" }, { "order": 648, @@ -411,7 +427,7 @@ "name": "italic", "prevSize": 32, "code": 59752, - "tempChar": "" + "tempChar": "" }, { "order": 620, @@ -419,7 +435,7 @@ "name": "link", "prevSize": 32, "code": 59753, - "tempChar": "" + "tempChar": "" }, { "order": 621, @@ -427,7 +443,7 @@ "name": "mention", "prevSize": 32, "code": 59754, - "tempChar": "" + "tempChar": "" }, { "order": 624, @@ -435,7 +451,7 @@ "name": "monospace", "prevSize": 32, "code": 59755, - "tempChar": "" + "tempChar": "" }, { "order": 625, @@ -443,7 +459,7 @@ "name": "next", "prevSize": 32, "code": 59756, - "tempChar": "" + "tempChar": "" }, { "order": 628, @@ -451,7 +467,7 @@ "name": "password-off", "prevSize": 32, "code": 59757, - "tempChar": "" + "tempChar": "" }, { "order": 629, @@ -459,7 +475,7 @@ "name": "pin-list", "prevSize": 32, "code": 59758, - "tempChar": "" + "tempChar": "" }, { "order": 632, @@ -467,7 +483,7 @@ "name": "previous", "prevSize": 32, "code": 59759, - "tempChar": "" + "tempChar": "" }, { "order": 633, @@ -475,7 +491,7 @@ "name": "replace", "prevSize": 32, "code": 59760, - "tempChar": "" + "tempChar": "" }, { "order": 636, @@ -483,7 +499,7 @@ "name": "schedule", "prevSize": 32, "code": 59761, - "tempChar": "" + "tempChar": "" }, { "order": 691, @@ -491,7 +507,7 @@ "name": "strikethrough", "prevSize": 32, "code": 59762, - "tempChar": "" + "tempChar": "" }, { "order": 692, @@ -499,7 +515,7 @@ "name": "underlined", "prevSize": 32, "code": 59763, - "tempChar": "" + "tempChar": "" }, { "order": 641, @@ -507,7 +523,7 @@ "name": "zoom-in", "prevSize": 32, "code": 59764, - "tempChar": "" + "tempChar": "" }, { "order": 649, @@ -515,20 +531,60 @@ "name": "zoom-out", "prevSize": 32, "code": 59765, - "tempChar": "" + "tempChar": "" } ], "id": 2, "metadata": { "name": "Untitled Set", "importSize": { - "width": 768, - "height": 768 + "width": 72, + "height": 72 } }, "height": 1024, "prevSize": 32, "icons": [ + { + "id": 48, + "paths": [ + "M829.156 277.333c12.8 0 25.6 27.022 28.444 41.244 15.644 89.6 41.244 147.911 46.933 238.933 2.844 34.133-28.444 146.489-49.778 167.822 2.844 1.422-21.333 25.6-69.689 73.956-28.444 28.444-65.422 41.244-102.4 42.667 5.689-4.267 11.378-8.533 17.067-14.222 48.356-48.356 75.378-78.222 78.222-86.756 21.333-21.333 51.2-76.8 48.356-110.933-5.689-75.378-28.444-159.289-45.511-236.089-1.422-9.956-1.422-24.178-2.844-39.822v-7.111c-1.422-22.756-2.844-42.667-2.844-42.667 1.422-5.689 4.267-11.378 8.533-15.644 5.689-5.689 14.222-9.956 21.333-9.956 0-1.422 11.378-1.422 24.178-1.422zM493.511 204.8c17.067-17.067 44.089-17.067 61.156 0 11.378 11.378 22.756 21.333 32.711 32.711 7.111 46.933 14.222 93.867 9.956 89.6-38.4-39.822-76.8-75.378-112.356-108.089 1.422-5.689 4.267-9.956 8.533-14.222z", + "M140.8 553.244l240.356 237.511c66.844 66.844 189.156 96.711 261.689 22.756 48.356-48.356 75.378-78.222 78.222-86.756 21.333-21.333 51.2-76.8 48.356-110.933-5.689-91.022-44.089-210.489-61.156-301.511-2.844-14.222-14.222-41.244-28.444-41.244-11.378 0-24.178 0-24.178 0-8.533 0-15.644 4.267-21.333 9.956-4.267 4.267-7.111 9.956-8.533 15.644 0 0 11.378 103.822 12.8 116.622s-7.111 17.067-17.067 8.533c-46.933-46.933-118.044-116.622-210.489-204.8-17.067-17.067-44.089-17.067-61.156 0s-17.067 44.089 0 61.156l142.222 142.222-19.911 19.911-190.578-193.422c-17.067-17.067-44.089-17.067-61.156 0s-17.067 44.089 0 61.156l193.422 193.422-19.911 19.911-173.511-173.511c-17.067-17.067-44.089-17.067-61.156 0s-17.067 44.089 0 61.156l173.511 172.089-19.911 21.333-112.356-112.356c-17.067-17.067-44.089-17.067-61.156 0-15.644 17.067-15.644 44.089 1.422 61.156z" + ], + "attrs": [ + {}, + {} + ], + "grid": 24, + "tags": [ + "reaction-filled" + ], + "isMulticolor": false, + "isMulticolor2": false + }, + { + "id": 47, + "paths": [ + "M541.257 911.848c-7.314 0-14.629 0-20.724-1.219-65.829-6.095-134.095-37.79-181.638-85.333l-249.905-249.905c-19.505-20.724-30.476-46.324-30.476-75.581 0-28.038 10.971-54.857 30.476-74.362 3.657-3.657 7.314-7.314 12.19-9.752-13.41-17.067-20.724-37.79-21.943-59.733v-3.657c0-29.257 10.971-56.076 30.476-75.581 9.752-9.752 20.724-17.067 32.914-23.162 0-1.219 0-2.438 0-4.876v-3.657c0-29.257 10.971-56.076 30.476-75.581 19.505-20.724 46.324-31.695 75.581-31.695 18.286 0 35.352 4.876 51.2 13.41 2.438-3.657 4.876-7.314 8.533-9.752l2.438-3.657c20.724-20.724 47.543-31.695 75.581-31.695s54.857 10.971 75.581 31.695c46.324 45.105 87.771 84.114 123.124 119.467 0-17.067 1.219-19.505 3.657-24.381l13.41-23.162 2.438-2.438c18.286-18.286 40.229-29.257 63.39-30.476 3.657 0 7.314 0 12.19 0s10.971 0 17.067-1.219c26.819 0 51.2 13.41 68.267 39.010 12.19 18.286 19.505 41.448 21.943 54.857 6.095 34.133 14.629 71.924 24.381 112.152 15.848 68.267 34.133 146.286 39.010 209.676 2.438 54.857-36.571 128-65.829 160.914-14.629 20.724-45.105 53.638-86.552 95.086s-96.305 63.39-157.257 64.61zM248.686 199.924c-12.19 0-23.162 4.876-31.695 13.41s-13.41 19.505-13.41 31.695v2.438c0 4.876 1.219 9.752 3.657 15.848l17.067 39.010-43.886 4.876c-9.752 1.219-19.505 4.876-26.819 13.41-8.533 8.533-13.41 19.505-13.41 31.695v2.438c0 10.971 4.876 20.724 13.41 29.257l62.171 63.39-60.952 9.752c-8.533 1.219-17.067 6.095-23.162 12.19-8.533 8.533-13.41 19.505-13.41 31.695s4.876 23.162 13.41 31.695l249.905 248.686c37.79 37.79 91.429 63.39 143.848 68.267 35.352 2.438 87.771-2.438 129.219-45.105 57.295-58.514 76.8-82.895 80.457-88.99l2.438-3.657c24.381-25.6 52.419-85.333 49.981-115.81-3.657-59.733-21.943-134.095-36.571-198.705-9.752-42.667-19.505-81.676-25.6-117.029-1.219-8.533-6.095-21.943-12.19-30.476s-12.19-12.19-15.848-12.19c0 0 0 0 0 0-6.095 0-10.971 0-15.848 1.219h-4.876c-1.219 0-3.657 0-4.876 0-6.095 0-13.41 2.438-21.943 9.752l-4.876 8.533c1.219 12.19 2.438 35.352 3.657 63.39l6.095 92.648-57.295-56.076c-46.324-46.324-107.276-104.838-180.419-175.543-18.286-18.286-47.543-17.067-63.39 0l-1.219 1.219c-4.876 6.095-8.533 13.41-10.971 21.943l-9.752 60.952-51.2-52.419c-8.533-8.533-19.505-13.41-31.695-13.41z", + "M753.371 917.943c-12.19 0-24.381-7.314-28.038-19.505-6.095-15.848 2.438-32.914 18.286-39.010s30.476-15.848 42.667-28.038c58.514-59.733 70.705-76.8 73.143-81.676l1.219-4.876 4.876-4.876c18.286-19.505 42.667-68.267 41.448-92.648-3.657-56.076-19.505-125.562-34.133-191.39-9.752-41.448-18.286-81.676-24.381-118.248-1.219-4.876-3.657-13.41-7.314-18.286-9.752-13.41-6.095-32.914 7.314-42.667s32.914-6.095 42.667 7.314c9.752 14.629 15.848 32.914 17.067 42.667 6.095 34.133 14.629 73.143 24.381 114.59 15.848 69.486 31.695 140.19 36.571 201.143 2.438 45.105-29.257 106.057-54.857 135.314-6.095 10.971-24.381 34.133-82.895 93.867-18.286 19.505-40.229 34.133-64.61 42.667-7.314 2.438-10.971 3.657-13.41 3.657zM856.99 749.714c0 0 0 0 0 0v0z", + "M314.514 605.867c-6.095 0-13.41-2.438-18.286-7.314l-146.286-158.476c-8.533-9.752-8.533-25.6 1.219-34.133s25.6-8.533 34.133 1.219l146.286 158.476c8.533 9.752 8.533 25.6-1.219 34.133-4.876 4.876-10.971 6.095-15.848 6.095z", + "M412.038 520.533c-6.095 0-12.19-2.438-17.067-7.314l-195.048-195.048c-9.752-9.752-9.752-24.381 0-34.133s24.381-9.752 34.133 0l195.048 195.048c9.752 9.752 9.752 24.381 0 34.133-4.876 6.095-10.971 7.314-17.067 7.314z", + "M497.371 423.010c-6.095 0-12.19-2.438-17.067-7.314l-195.048-195.048c-9.752-9.752-9.752-24.381 0-34.133s24.381-9.752 34.133 0l195.048 195.048c9.752 9.752 9.752 24.381 0 34.133-4.876 6.095-10.971 7.314-17.067 7.314z" + ], + "attrs": [ + {}, + {}, + {}, + {}, + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "reactions" + ] + }, { "id": 46, "paths": [ @@ -2811,7 +2867,7 @@ "name": "select", "prevSize": 32, "code": 59744, - "tempChar": "" + "tempChar": "" }, { "order": 480, @@ -2819,7 +2875,7 @@ "name": "folder", "prevSize": 32, "code": 59667, - "tempChar": "" + "tempChar": "" }, { "order": 481, @@ -2827,7 +2883,7 @@ "name": "bots", "prevSize": 32, "code": 59669, - "tempChar": "" + "tempChar": "" }, { "order": 482, @@ -2835,7 +2891,7 @@ "name": "calendar", "prevSize": 32, "code": 59670, - "tempChar": "" + "tempChar": "" }, { "order": 483, @@ -2843,7 +2899,7 @@ "name": "cloud-download", "prevSize": 32, "code": 59671, - "tempChar": "" + "tempChar": "" }, { "order": 484, @@ -2851,7 +2907,7 @@ "name": "colorize", "prevSize": 32, "code": 59672, - "tempChar": "" + "tempChar": "" }, { "order": 651, @@ -2859,7 +2915,7 @@ "name": "forward", "prevSize": 32, "code": 59687, - "tempChar": "" + "tempChar": "" }, { "order": 650, @@ -2867,7 +2923,7 @@ "name": "reply", "prevSize": 32, "code": 59719, - "tempChar": "" + "tempChar": "" }, { "order": 487, @@ -2875,7 +2931,7 @@ "name": "help", "prevSize": 32, "code": 59690, - "tempChar": "" + "tempChar": "" }, { "order": 488, @@ -2883,7 +2939,7 @@ "name": "info", "prevSize": 32, "code": 59691, - "tempChar": "" + "tempChar": "" }, { "order": 489, @@ -2891,7 +2947,7 @@ "name": "info-filled", "prevSize": 32, "code": 59675, - "tempChar": "" + "tempChar": "" }, { "order": 490, @@ -2899,7 +2955,7 @@ "name": "delete-filled", "prevSize": 32, "code": 59676, - "tempChar": "" + "tempChar": "" }, { "order": 491, @@ -2907,7 +2963,7 @@ "name": "delete", "prevSize": 32, "code": 59677, - "tempChar": "" + "tempChar": "" }, { "order": 492, @@ -2915,7 +2971,7 @@ "name": "edit", "prevSize": 32, "code": 59683, - "tempChar": "" + "tempChar": "" }, { "order": 493, @@ -2923,7 +2979,7 @@ "name": "new-chat-filled", "prevSize": 32, "code": 59705, - "tempChar": "" + "tempChar": "" }, { "order": 494, @@ -2931,7 +2987,7 @@ "name": "send", "prevSize": 32, "code": 59722, - "tempChar": "" + "tempChar": "" }, { "order": 495, @@ -2939,7 +2995,7 @@ "name": "send-outline", "prevSize": 32, "code": 59723, - "tempChar": "" + "tempChar": "" }, { "order": 496, @@ -2947,7 +3003,7 @@ "name": "add-user-filled", "prevSize": 32, "code": 59652, - "tempChar": "" + "tempChar": "" }, { "order": 497, @@ -2955,7 +3011,7 @@ "name": "add-user", "prevSize": 32, "code": 59653, - "tempChar": "" + "tempChar": "" }, { "order": 498, @@ -2963,7 +3019,7 @@ "name": "delete-user", "prevSize": 32, "code": 59678, - "tempChar": "" + "tempChar": "" }, { "order": 499, @@ -2971,7 +3027,7 @@ "name": "microphone", "prevSize": 32, "code": 59701, - "tempChar": "" + "tempChar": "" }, { "order": 500, @@ -2979,7 +3035,7 @@ "name": "microphone-alt", "prevSize": 32, "code": 59707, - "tempChar": "" + "tempChar": "" }, { "order": 501, @@ -2987,7 +3043,7 @@ "name": "poll", "prevSize": 32, "code": 59704, - "tempChar": "" + "tempChar": "" }, { "order": 502, @@ -2995,7 +3051,7 @@ "name": "revote", "prevSize": 32, "code": 59706, - "tempChar": "" + "tempChar": "" }, { "order": 503, @@ -3003,7 +3059,7 @@ "name": "photo", "prevSize": 32, "code": 59712, - "tempChar": "" + "tempChar": "" }, { "order": 504, @@ -3011,7 +3067,7 @@ "name": "document", "prevSize": 32, "code": 59679, - "tempChar": "" + "tempChar": "" }, { "order": 505, @@ -3019,7 +3075,7 @@ "name": "camera", "prevSize": 32, "code": 59662, - "tempChar": "" + "tempChar": "" }, { "order": 506, @@ -3027,7 +3083,7 @@ "name": "camera-add", "prevSize": 32, "code": 59663, - "tempChar": "" + "tempChar": "" }, { "order": 507, @@ -3035,7 +3091,7 @@ "name": "logout", "prevSize": 32, "code": 59698, - "tempChar": "" + "tempChar": "" }, { "order": 508, @@ -3043,7 +3099,7 @@ "name": "saved-messages", "prevSize": 32, "code": 59720, - "tempChar": "" + "tempChar": "" }, { "order": 509, @@ -3051,7 +3107,7 @@ "name": "settings", "prevSize": 32, "code": 59726, - "tempChar": "" + "tempChar": "" }, { "order": 652, @@ -3059,7 +3115,7 @@ "name": "phone", "prevSize": 32, "code": 59711, - "tempChar": "" + "tempChar": "" }, { "order": 653, @@ -3067,7 +3123,7 @@ "name": "attach", "prevSize": 32, "code": 59657, - "tempChar": "" + "tempChar": "" }, { "order": 512, @@ -3075,7 +3131,7 @@ "name": "copy", "prevSize": 32, "code": 59674, - "tempChar": "" + "tempChar": "" }, { "order": 513, @@ -3083,7 +3139,7 @@ "name": "channel", "prevSize": 32, "code": 59665, - "tempChar": "" + "tempChar": "" }, { "order": 514, @@ -3091,7 +3147,7 @@ "name": "group", "prevSize": 32, "code": 59689, - "tempChar": "" + "tempChar": "" }, { "order": 515, @@ -3099,7 +3155,7 @@ "name": "user", "prevSize": 32, "code": 59737, - "tempChar": "" + "tempChar": "" }, { "order": 516, @@ -3107,7 +3163,7 @@ "name": "non-contacts", "prevSize": 32, "code": 59688, - "tempChar": "" + "tempChar": "" }, { "order": 517, @@ -3115,7 +3171,7 @@ "name": "active-sessions", "prevSize": 32, "code": 59650, - "tempChar": "" + "tempChar": "" }, { "order": 518, @@ -3123,7 +3179,7 @@ "name": "admin", "prevSize": 32, "code": 59654, - "tempChar": "" + "tempChar": "" }, { "order": 519, @@ -3131,7 +3187,7 @@ "name": "download", "prevSize": 32, "code": 59681, - "tempChar": "" + "tempChar": "" }, { "order": 520, @@ -3139,7 +3195,7 @@ "name": "location", "prevSize": 32, "code": 59696, - "tempChar": "" + "tempChar": "" }, { "order": 521, @@ -3147,7 +3203,7 @@ "name": "stop", "prevSize": 32, "code": 59730, - "tempChar": "" + "tempChar": "" }, { "order": 523, @@ -3155,7 +3211,7 @@ "name": "archive", "prevSize": 32, "code": 59656, - "tempChar": "" + "tempChar": "" }, { "order": 524, @@ -3163,7 +3219,7 @@ "name": "unarchive", "prevSize": 32, "code": 59731, - "tempChar": "" + "tempChar": "" }, { "order": 525, @@ -3171,7 +3227,7 @@ "name": "readchats", "prevSize": 32, "code": 59699, - "tempChar": "" + "tempChar": "" }, { "order": 526, @@ -3179,7 +3235,7 @@ "name": "unread", "prevSize": 32, "code": 59735, - "tempChar": "" + "tempChar": "" }, { "order": 654, @@ -3187,7 +3243,7 @@ "name": "message", "prevSize": 32, "code": 59700, - "tempChar": "" + "tempChar": "" }, { "order": 659, @@ -3195,7 +3251,7 @@ "name": "lock", "prevSize": 32, "code": 59697, - "tempChar": "" + "tempChar": "" }, { "order": 529, @@ -3203,7 +3259,7 @@ "name": "unlock", "prevSize": 32, "code": 59732, - "tempChar": "" + "tempChar": "" }, { "order": 530, @@ -3211,7 +3267,7 @@ "name": "mute", "prevSize": 32, "code": 59703, - "tempChar": "" + "tempChar": "" }, { "order": 531, @@ -3219,7 +3275,7 @@ "name": "unmute", "prevSize": 32, "code": 59733, - "tempChar": "" + "tempChar": "" }, { "order": 532, @@ -3227,7 +3283,7 @@ "name": "pin", "prevSize": 32, "code": 59713, - "tempChar": "" + "tempChar": "" }, { "order": 533, @@ -3235,7 +3291,7 @@ "name": "unpin", "prevSize": 32, "code": 59734, - "tempChar": "" + "tempChar": "" }, { "order": 534, @@ -3243,7 +3299,7 @@ "name": "smallscreen", "prevSize": 32, "code": 59742, - "tempChar": "" + "tempChar": "" }, { "order": 535, @@ -3251,7 +3307,7 @@ "name": "fullscreen", "prevSize": 32, "code": 59743, - "tempChar": "" + "tempChar": "" }, { "order": 536, @@ -3259,7 +3315,7 @@ "name": "large-pause", "prevSize": 32, "code": 59694, - "tempChar": "" + "tempChar": "" }, { "order": 537, @@ -3267,7 +3323,7 @@ "name": "large-play", "prevSize": 32, "code": 59695, - "tempChar": "" + "tempChar": "" }, { "order": 538, @@ -3275,7 +3331,7 @@ "name": "pause", "prevSize": 32, "code": 59709, - "tempChar": "" + "tempChar": "" }, { "order": 539, @@ -3283,7 +3339,7 @@ "name": "play", "prevSize": 32, "code": 59715, - "tempChar": "" + "tempChar": "" }, { "order": 540, @@ -3291,7 +3347,7 @@ "name": "channelviews", "prevSize": 32, "code": 59666, - "tempChar": "" + "tempChar": "" }, { "order": 541, @@ -3299,7 +3355,7 @@ "name": "message-succeeded", "prevSize": 32, "code": 59648, - "tempChar": "" + "tempChar": "" }, { "order": 657, @@ -3307,7 +3363,7 @@ "name": "message-read", "prevSize": 32, "code": 59649, - "tempChar": "" + "tempChar": "" }, { "order": 543, @@ -3315,7 +3371,7 @@ "name": "message-pending", "prevSize": 32, "code": 59724, - "tempChar": "" + "tempChar": "" }, { "order": 544, @@ -3323,7 +3379,7 @@ "name": "message-failed", "prevSize": 32, "code": 59725, - "tempChar": "" + "tempChar": "" }, { "order": 545, @@ -3331,7 +3387,7 @@ "name": "favorite", "prevSize": 32, "code": 59710, - "tempChar": "" + "tempChar": "" }, { "order": 546, @@ -3339,7 +3395,7 @@ "name": "keyboard", "prevSize": 32, "code": 59716, - "tempChar": "" + "tempChar": "" }, { "order": 547, @@ -3347,7 +3403,7 @@ "name": "delete-left", "prevSize": 32, "code": 59717, - "tempChar": "" + "tempChar": "" }, { "order": 548, @@ -3355,7 +3411,7 @@ "name": "recent", "prevSize": 32, "code": 59718, - "tempChar": "" + "tempChar": "" }, { "order": 549, @@ -3363,7 +3419,7 @@ "name": "gifs", "prevSize": 32, "code": 59727, - "tempChar": "" + "tempChar": "" }, { "order": 550, @@ -3371,7 +3427,7 @@ "name": "stickers", "prevSize": 32, "code": 59739, - "tempChar": "" + "tempChar": "" }, { "order": 551, @@ -3379,7 +3435,7 @@ "name": "smile", "prevSize": 32, "code": 59728, - "tempChar": "" + "tempChar": "" }, { "order": 552, @@ -3387,7 +3443,7 @@ "name": "animals", "prevSize": 32, "code": 59655, - "tempChar": "" + "tempChar": "" }, { "order": 553, @@ -3395,7 +3451,7 @@ "name": "eats", "prevSize": 32, "code": 59682, - "tempChar": "" + "tempChar": "" }, { "order": 554, @@ -3403,7 +3459,7 @@ "name": "sport", "prevSize": 32, "code": 59729, - "tempChar": "" + "tempChar": "" }, { "order": 555, @@ -3411,7 +3467,7 @@ "name": "car", "prevSize": 32, "code": 59664, - "tempChar": "" + "tempChar": "" }, { "order": 556, @@ -3419,7 +3475,7 @@ "name": "lamp", "prevSize": 32, "code": 59692, - "tempChar": "" + "tempChar": "" }, { "order": 557, @@ -3427,7 +3483,7 @@ "name": "language", "prevSize": 32, "code": 59693, - "tempChar": "" + "tempChar": "" }, { "order": 558, @@ -3435,7 +3491,7 @@ "name": "flag", "prevSize": 32, "code": 59686, - "tempChar": "" + "tempChar": "" }, { "order": 559, @@ -3443,7 +3499,7 @@ "name": "more", "prevSize": 32, "code": 59702, - "tempChar": "" + "tempChar": "" }, { "order": 560, @@ -3451,7 +3507,7 @@ "name": "search", "prevSize": 32, "code": 59721, - "tempChar": "" + "tempChar": "" }, { "order": 561, @@ -3459,7 +3515,7 @@ "name": "remove", "prevSize": 32, "code": 59740, - "tempChar": "" + "tempChar": "" }, { "order": 562, @@ -3467,7 +3523,7 @@ "name": "add", "prevSize": 32, "code": 59651, - "tempChar": "" + "tempChar": "" }, { "order": 563, @@ -3475,7 +3531,7 @@ "name": "check", "prevSize": 32, "code": 59668, - "tempChar": "" + "tempChar": "" }, { "order": 564, @@ -3483,7 +3539,7 @@ "name": "close", "prevSize": 32, "code": 59673, - "tempChar": "" + "tempChar": "" }, { "order": 610, @@ -3491,7 +3547,7 @@ "name": "arrow-left", "prevSize": 32, "code": 59661, - "tempChar": "" + "tempChar": "" }, { "order": 566, @@ -3499,7 +3555,7 @@ "name": "arrow-right", "prevSize": 32, "code": 59708, - "tempChar": "" + "tempChar": "" }, { "order": 567, @@ -3507,7 +3563,7 @@ "name": "down", "prevSize": 32, "code": 59680, - "tempChar": "" + "tempChar": "" }, { "order": 568, @@ -3515,7 +3571,7 @@ "name": "up", "prevSize": 32, "code": 59736, - "tempChar": "" + "tempChar": "" }, { "order": 569, @@ -3523,7 +3579,7 @@ "name": "eye-closed", "prevSize": 32, "code": 59685, - "tempChar": "" + "tempChar": "" }, { "order": 570, @@ -3531,7 +3587,7 @@ "name": "eye", "prevSize": 32, "code": 59684, - "tempChar": "" + "tempChar": "" }, { "order": 571, @@ -3539,7 +3595,7 @@ "name": "muted-chat", "prevSize": 32, "code": 59741, - "tempChar": "" + "tempChar": "" }, { "order": 572, @@ -3547,7 +3603,7 @@ "name": "avatar-archived-chats", "prevSize": 32, "code": 59658, - "tempChar": "" + "tempChar": "" }, { "order": 573, @@ -3555,7 +3611,7 @@ "name": "avatar-deleted-account", "prevSize": 32, "code": 59659, - "tempChar": "" + "tempChar": "" }, { "order": 574, @@ -3563,7 +3619,7 @@ "name": "avatar-saved-messages", "prevSize": 32, "code": 59660, - "tempChar": "" + "tempChar": "" }, { "order": 575, @@ -3571,7 +3627,7 @@ "name": "pinned-chat", "prevSize": 32, "code": 59714, - "tempChar": "" + "tempChar": "" } ], "prevSize": 32, diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 72ecc8983..e7befe130 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -56,6 +56,11 @@ $color-user-6: #ee7aae; $color-user-7: #6ec9cb; $color-user-8: #faa774; +$color-message-reaction: #ebf3fd; +$color-message-reaction-hover: #c5def9; +$color-message-reaction-own: #cef0ba; +$color-message-reaction-own-hover: #b5e0a4; + :root { --color-background: #{$color-white}; --color-background-selected: #f4f4f5; @@ -116,6 +121,11 @@ $color-user-8: #faa774; --color-accent-own: #{$color-text-green}; --color-message-meta-own: #{$color-text-green}; + --color-message-reaction: $color-message-reaction; + --color-message-reaction-hover: $color-message-reaction-hover; + --color-message-reaction-own: $color-message-reaction-own; + --color-message-reaction-hover-own: $color-message-reaction-own-hover; + --color-reply-hover: #{blend-normal(rgba($color-text-secondary, 0.08), $color-white)}; --color-reply-active: #{blend-normal(rgba($color-text-secondary, 0.16), $color-white)}; --color-reply-own-hover: #{blend-normal(rgba($color-text-green, 0.12), $color-light-green)}; diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 7668eb52f..82ab7bd23 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -50,6 +50,12 @@ .icon-volume-3:before { content: "\e991"; } +.icon-reaction-filled:before { + content: "\e994"; +} +.icon-reactions:before { + content: "\e993"; +} .icon-sidebar:before { content: "\e992"; } diff --git a/src/styles/themes.json b/src/styles/themes.json index 9610f2362..18103b19c 100644 --- a/src/styles/themes.json +++ b/src/styles/themes.json @@ -38,5 +38,9 @@ "--color-code-own": ["#3c7940", "#FFFFFF"], "--color-code-bg": ["#70757914", "#ffffff26"], "--color-code-own-bg": ["#70757914", "#ffffff26"], - "--color-composer-button": ["#707579CC", "#AAAAAACC"] + "--color-composer-button": ["#707579CC", "#AAAAAACC"], + "--color-message-reaction": ["#ebf3fd", "#2b2a35"], + "--color-message-reaction-hover": ["#c5def9", "#343147"], + "--color-message-reaction-own": ["#cef0ba", "#7a68ca"], + "--color-message-reaction-hover-own": ["#b5e0a4", "#7567bc"] } diff --git a/src/types/index.ts b/src/types/index.ts index deaa7d56e..0663b5fb6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -201,6 +201,7 @@ export enum SettingsScreens { TwoFaRecoveryEmail, TwoFaRecoveryEmailCode, TwoFaCongratulations, + QuickReaction, } export type StickerSetOrRecent = Pick = { Down: true, @@ -32,7 +33,9 @@ function isTextBox(target: EventTarget | null) { return inputTypes.indexOf(type.toLowerCase()) > -1; } -const getTouchY = (e: WheelEvent | TouchEvent) => ('changedTouches' in e ? e.changedTouches[0].clientY : 0); +export const getTouchY = (e: WheelEvent | TouchEvent | React.WheelEvent | React.TouchEvent) => { + return ('changedTouches' in e ? e.changedTouches[0].clientY : 0); +}; const preventDefault = (e: WheelEvent | TouchEvent) => { const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e); @@ -46,6 +49,7 @@ const preventDefault = (e: WheelEvent | TouchEvent) => { // Prevent bottom overscroll || (scrollLockEl.scrollTop >= (scrollLockEl.scrollHeight - scrollLockEl.offsetHeight) && deltaY >= 0) ) { + if (excludedClosestSelector && (e.target as HTMLElement).closest(excludedClosestSelector)) return; e.preventDefault(); } }; @@ -56,8 +60,9 @@ function preventDefaultForScrollKeys(e: KeyboardEvent) { } } -export function disableScrolling(el?: HTMLElement | null) { +export function disableScrolling(el?: HTMLElement | null, _excludedClosestSelector?: string) { scrollLockEl = el; + excludedClosestSelector = _excludedClosestSelector; // Disable scrolling in Chrome document.addEventListener('wheel', preventDefault, { passive: false }); document.addEventListener('touchmove', preventDefault, { passive: false }); @@ -66,6 +71,7 @@ export function disableScrolling(el?: HTMLElement | null) { export function enableScrolling() { scrollLockEl = undefined; + excludedClosestSelector = undefined; document.removeEventListener('wheel', preventDefault); // Enable scrolling in Chrome document.removeEventListener('touchmove', preventDefault); // eslint-disable-next-line no-null/no-null