diff --git a/submodules/Emoji/Sources/EmojiUtils.swift b/submodules/Emoji/Sources/EmojiUtils.swift index fd58bb060f..f77fead2b2 100644 --- a/submodules/Emoji/Sources/EmojiUtils.swift +++ b/submodules/Emoji/Sources/EmojiUtils.swift @@ -5,7 +5,7 @@ import AVFoundation public extension UnicodeScalar { var isEmoji: Bool { switch self.value { - case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8, 0x1FA70...0x1FA73, 0x1FA78...0x1FA7A, 0x1FA80...0x1FA82, 0x1FA90...0x1FA95, 0x1F382: + case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8, 0x1FA70...0x1FA73, 0x1FA78...0x1FA7A, 0x1FA80...0x1FA82, 0x1FA90...0x1FA95, 0x1F382, 0x1FAF1, 0x1FAF2: return true case 0x2603, 0x265F, 0x267E, 0x2692, 0x26C4, 0x26C8, 0x26CE, 0x26CF, 0x26D1...0x26D3, 0x26E9, 0x26F0...0x26F9, 0x2705, 0x270A, 0x270B, 0x2728, 0x274E, 0x2753...0x2755, 0x274C, 0x2795...0x2797, 0x27B0, 0x27BF: return true diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 3c6a0446e5..ef2a5abe34 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -543,12 +543,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var emojiFile: TelegramMediaFile? var emojiString: String? - if let entities = item.message.textEntitiesAttribute?.entities { - if entities.count == 1, case let .CustomEmoji(_, fileId) = entities[0].type { - if let file = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { - emojiFile = file - } - } else if messageIsElligibleForLargeCustomEmoji(item.message) { + if let _ = item.message.textEntitiesAttribute?.entities { + if messageIsElligibleForLargeCustomEmoji(item.message) { emojiString = item.message.text } } @@ -566,12 +562,18 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else if self.emojiFile?.id != emojiFile?.id { self.emojiFile = emojiFile if let emojiFile = emojiFile { - let dimensions = emojiFile.dimensions ?? PixelDimensions(width: 512, height: 512) + var dimensions = emojiFile.dimensions ?? PixelDimensions(width: 512, height: 512) + if emojiFile.isCustomEmoji { + dimensions = PixelDimensions(dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))) + } var fitzModifier: EmojiFitzModifier? if let fitz = fitz { fitzModifier = EmojiFitzModifier(emoji: fitz) } - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) + + let fillSize = emojiFile.isCustomEmoji ? CGSize(width: 512.0, height: 512.0) : CGSize(width: 384.0, height: 384.0) + + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(fillSize), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) } @@ -667,7 +669,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.didSetUpAnimationNode = true if let file = file { - let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + var dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + if file.isCustomEmoji { + dimensions = PixelDimensions(dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))) + } let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) @@ -703,7 +708,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if !alreadySeen { item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) - if let file = file, file.isCustomEmoji { + if let emojiString = self.emojiString, emojiString.count == 1 { self.playAdditionalEmojiAnimation(index: 1) } else if let file = file, file.isPremiumSticker { Queue.mainQueue().after(0.1) { @@ -940,7 +945,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { isEmoji = true let displaySize = CGSize(width: floor(displaySize.width * item.presentationData.animatedEmojiScale), height: floor(displaySize.height * item.presentationData.animatedEmojiScale)) - if let dimensions = emojiFile.dimensions { + + if var dimensions = emojiFile.dimensions { + if emojiFile.isCustomEmoji { + dimensions = PixelDimensions(dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))) + } imageSize = CGSize(width: displaySize.width * CGFloat(dimensions.width) / 512.0, height: displaySize.height * CGFloat(dimensions.height) / 512.0) } else if let thumbnailSize = emojiFile.previewRepresentations.first?.dimensions { imageSize = thumbnailSize.cgSize.aspectFitted(displaySize) @@ -1744,7 +1753,21 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let source = AnimatedStickerResourceSource(account: item.context.account, resource: resource, fitzModifier: nil) - guard let animationSize = self.animationSize, let animationNode = self.animationNode else { + + let animationSize: CGSize? + let animationNodeFrame: CGRect? + if let size = self.animationSize, let node = self.animationNode { + animationSize = size + animationNodeFrame = node.frame + } else if let _ = self.emojiString { + animationSize = CGSize(width: 384.0, height: 384.0) + animationNodeFrame = self.textNode.textNode.frame + } else { + animationSize = nil + animationNodeFrame = nil + } + + guard let animationSize = animationSize, let animationNodeFrame = animationNodeFrame else { return } if self.additionalAnimationNodes.count >= 4 { @@ -1762,8 +1785,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { overlayMeshAnimationNode = current } else { if let animationView = MeshRenderer() { - let animationFrame = animationNode.frame.insetBy(dx: -animationNode.frame.width, dy: -animationNode.frame.height) - .offsetBy(dx: incomingMessage ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0) + let animationFrame = animationNodeFrame.insetBy(dx: -animationNodeFrame.width, dy: -animationNodeFrame.height) + .offsetBy(dx: incomingMessage ? animationNodeFrame.width - 10.0 : -animationNodeFrame.width + 10.0, dy: 0.0) animationView.frame = animationFrame animationView.allAnimationsCompleted = { [weak transitionNode, weak animationView, weak self] in @@ -1794,10 +1817,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var animationFrame: CGRect if isStickerEffect { let scale: CGFloat = 0.245 - animationFrame = animationNode.frame.offsetBy(dx: incomingMessage ? animationNode.frame.width * scale - 21.0 : -animationNode.frame.width * scale + 21.0, dy: -1.0).insetBy(dx: -animationNode.frame.width * scale, dy: -animationNode.frame.height * scale) + animationFrame = animationNodeFrame.offsetBy(dx: incomingMessage ? animationNodeFrame.width * scale - 21.0 : -animationNodeFrame.width * scale + 21.0, dy: -1.0).insetBy(dx: -animationNodeFrame.width * scale, dy: -animationNodeFrame.height * scale) } else { - animationFrame = animationNode.frame.insetBy(dx: -animationNode.frame.width, dy: -animationNode.frame.height) - .offsetBy(dx: incomingMessage ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0) + animationFrame = animationNodeFrame.insetBy(dx: -animationNodeFrame.width, dy: -animationNodeFrame.height) + .offsetBy(dx: incomingMessage ? animationNodeFrame.width - 10.0 : -animationNodeFrame.width + 10.0, dy: 0.0) animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0)) } @@ -1881,8 +1904,189 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } } - + if let item = self.item, self.imageNode.frame.contains(location) { + let emojiTapAction: (Bool) -> InternalBubbleTapAction? = { shouldPlay in + let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D] + let heart = 0x2764 + let peach = 0x1F351 + let coffin = 0x26B0 + + let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> take(1) + |> map { view in + return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + } + + let text = item.message.text + if var firstScalar = text.unicodeScalars.first { + var textEmoji = text.strippedEmoji + var additionalTextEmoji = textEmoji + if beatingHearts.contains(firstScalar.value) { + textEmoji = "โค๏ธ" + firstScalar = UnicodeScalar(heart)! + } + + let (basicEmoji, fitz) = text.basicEmoji + if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค", "๐ŸคŽ", "๐Ÿค", "โค๏ธ"].contains(textEmoji) { + additionalTextEmoji = "โค๏ธ".strippedEmoji + } else if fitz != nil { + additionalTextEmoji = basicEmoji + } + + let syncAnimations = item.message.id.peerId.namespace == Namespaces.Peer.CloudUser + + return .optionalAction({ + var haptic: EmojiHaptic? + if let current = self.haptic { + haptic = current + } else { + if firstScalar.value == heart { + haptic = HeartbeatHaptic() + } else if firstScalar.value == coffin { + haptic = CoffinHaptic() + } else if firstScalar.value == peach { + haptic = PeachHaptic() + } + haptic?.enabled = true + self.haptic = haptic + } + + if syncAnimations, let animationItems = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji] { + let playHaptic = haptic == nil + + var hapticFeedback: HapticFeedback + if let current = self.hapticFeedback { + hapticFeedback = current + } else { + hapticFeedback = HapticFeedback() + self.hapticFeedback = hapticFeedback + } + + if syncAnimations { + self.startAdditionalAnimationsCommitTimer() + } + + let timestamp = CACurrentMediaTime() + let previousAnimation = self.enqueuedAdditionalAnimations.last + + var availableAnimations = animationItems + var delay: Double = 0.0 + if availableAnimations.count > 1, let (previousIndex, _) = previousAnimation { + availableAnimations.removeValue(forKey: previousIndex) + } + if let (_, previousTimestamp) = previousAnimation { + delay = min(0.15, max(0.0, previousTimestamp + 0.15 - timestamp)) + } + if let index = availableAnimations.randomElement()?.0 { + if delay > 0.0 { + Queue.mainQueue().after(delay) { + if playHaptic { + if previousAnimation == nil { + hapticFeedback.impact(.light) + } else { + let style: ImpactHapticFeedbackStyle + if self.enqueuedAdditionalAnimations.count == 1 { + style = .medium + } else { + style = [.light, .medium].randomElement() ?? .medium + } + hapticFeedback.impact(style) + } + } + + if syncAnimations { + self.enqueuedAdditionalAnimations.append((index, timestamp + delay)) + } + self.playAdditionalEmojiAnimation(index: index) + + if syncAnimations, self.additionalAnimationsCommitTimer == nil { + self.startAdditionalAnimationsCommitTimer() + } + } + } else { + if playHaptic { + if previousAnimation == nil { + hapticFeedback.impact(.light) + } else { + let style: ImpactHapticFeedbackStyle + if self.enqueuedAdditionalAnimations.count == 1 { + style = .medium + } else { + style = [.light, .medium].randomElement() ?? .medium + } + hapticFeedback.impact(style) + } + } + + if syncAnimations { + self.enqueuedAdditionalAnimations.append((index, timestamp)) + } + self.playAdditionalEmojiAnimation(index: index) + } + } + } else if let emojiString = self.emojiString, emojiString.count == 1 { + let _ = item.controllerInteraction.openMessage(item.message, .default) + } + + if shouldPlay { + let _ = (appConfiguration + |> deliverOnMainQueue).start(next: { [weak self] appConfiguration in + guard let strongSelf = self else { + return + } + let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account) + var hasSound = false + for (emoji, file) in emojiSounds.sounds { + if emoji.strippedEmoji == textEmoji.strippedEmoji { + hasSound = true + let mediaManager = item.context.sharedContext.mediaManager + let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true) + mediaPlayer.togglePlayPause() + mediaPlayer.actionAtEnd = .action({ [weak self] in + self?.mediaPlayer = nil + }) + strongSelf.mediaPlayer = mediaPlayer + + strongSelf.mediaStatusDisposable.set((mediaPlayer.status + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + if let haptic = haptic, !haptic.active { + haptic.start(time: 0.0) + } + + switch status.status { + case .playing: + if let animationNode = strongSelf.animationNode as? AnimatedStickerNode { + animationNode.play(firstFrame: false, fromIndex: nil) + } + strongSelf.mediaStatusDisposable.set(nil) + default: + break + } + } + })) + return + } + } + if !hasSound { + if let haptic = haptic, !haptic.active { + haptic.start(time: 0.0) + } + if let animationNode = strongSelf.animationNode as? AnimatedStickerNode { + animationNode.play(firstFrame: false, fromIndex: nil) + } + } + }) + } + }) + } + return nil + } + + if let emojiString = self.emojiString, emojiString.count == 1 { + return emojiTapAction(false) + } if let file = self.telegramFile { let noPremium = item.message.attributes.contains(where: { attribute in if attribute is NonPremiumMessageAttribute { @@ -1916,176 +2120,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if !animationNode.isPlaying && !emojiFile.isCustomEmoji { shouldPlay = true } - - let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D] - let heart = 0x2764 - let peach = 0x1F351 - let coffin = 0x26B0 - - let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> take(1) - |> map { view in - return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue - } - - let text = item.message.text - if var firstScalar = text.unicodeScalars.first { - var textEmoji = text.strippedEmoji - var additionalTextEmoji = textEmoji - if beatingHearts.contains(firstScalar.value) { - textEmoji = "โค๏ธ" - firstScalar = UnicodeScalar(heart)! - } - - let (basicEmoji, fitz) = text.basicEmoji - if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค", "๐ŸคŽ", "๐Ÿค", "โค๏ธ"].contains(textEmoji) { - additionalTextEmoji = "โค๏ธ".strippedEmoji - } else if fitz != nil { - additionalTextEmoji = basicEmoji - } - - let syncAnimations = item.message.id.peerId.namespace == Namespaces.Peer.CloudUser - - return .optionalAction({ - var haptic: EmojiHaptic? - if let current = self.haptic { - haptic = current - } else { - if firstScalar.value == heart { - haptic = HeartbeatHaptic() - } else if firstScalar.value == coffin { - haptic = CoffinHaptic() - } else if firstScalar.value == peach { - haptic = PeachHaptic() - } - haptic?.enabled = true - self.haptic = haptic - } - - if syncAnimations, let animationItems = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji] { - let playHaptic = haptic == nil - - var hapticFeedback: HapticFeedback - if let current = self.hapticFeedback { - hapticFeedback = current - } else { - hapticFeedback = HapticFeedback() - self.hapticFeedback = hapticFeedback - } - - if syncAnimations { - self.startAdditionalAnimationsCommitTimer() - } - - let timestamp = CACurrentMediaTime() - let previousAnimation = self.enqueuedAdditionalAnimations.last - - var availableAnimations = animationItems - var delay: Double = 0.0 - if availableAnimations.count > 1, let (previousIndex, _) = previousAnimation { - availableAnimations.removeValue(forKey: previousIndex) - } - if let (_, previousTimestamp) = previousAnimation { - delay = min(0.15, max(0.0, previousTimestamp + 0.15 - timestamp)) - } - if let index = availableAnimations.randomElement()?.0 { - if delay > 0.0 { - Queue.mainQueue().after(delay) { - if playHaptic { - if previousAnimation == nil { - hapticFeedback.impact(.light) - } else { - let style: ImpactHapticFeedbackStyle - if self.enqueuedAdditionalAnimations.count == 1 { - style = .medium - } else { - style = [.light, .medium].randomElement() ?? .medium - } - hapticFeedback.impact(style) - } - } - - if syncAnimations { - self.enqueuedAdditionalAnimations.append((index, timestamp + delay)) - } - self.playAdditionalEmojiAnimation(index: index) - - if syncAnimations, self.additionalAnimationsCommitTimer == nil { - self.startAdditionalAnimationsCommitTimer() - } - } - } else { - if playHaptic { - if previousAnimation == nil { - hapticFeedback.impact(.light) - } else { - let style: ImpactHapticFeedbackStyle - if self.enqueuedAdditionalAnimations.count == 1 { - style = .medium - } else { - style = [.light, .medium].randomElement() ?? .medium - } - hapticFeedback.impact(style) - } - } - - if syncAnimations { - self.enqueuedAdditionalAnimations.append((index, timestamp)) - } - self.playAdditionalEmojiAnimation(index: index) - } - } - } else if emojiFile.isCustomEmoji { - let _ = item.controllerInteraction.openMessage(item.message, .default) - } - - if shouldPlay { - let _ = (appConfiguration - |> deliverOnMainQueue).start(next: { [weak self, weak animationNode] appConfiguration in - guard let strongSelf = self else { - return - } - let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account) - var hasSound = false - for (emoji, file) in emojiSounds.sounds { - if emoji.strippedEmoji == textEmoji.strippedEmoji { - hasSound = true - let mediaManager = item.context.sharedContext.mediaManager - let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true) - mediaPlayer.togglePlayPause() - mediaPlayer.actionAtEnd = .action({ [weak self] in - self?.mediaPlayer = nil - }) - strongSelf.mediaPlayer = mediaPlayer - - strongSelf.mediaStatusDisposable.set((mediaPlayer.status - |> deliverOnMainQueue).start(next: { [weak self, weak animationNode] status in - if let strongSelf = self { - if let haptic = haptic, !haptic.active { - haptic.start(time: 0.0) - } - - switch status.status { - case .playing: - animationNode?.play(firstFrame: false, fromIndex: nil) - strongSelf.mediaStatusDisposable.set(nil) - default: - break - } - } - })) - return - } - } - if !hasSound { - if let haptic = haptic, !haptic.active { - haptic.start(time: 0.0) - } - animationNode?.play(firstFrame: false, fromIndex: nil) - } - }) - } - }) + if let result = emojiTapAction(shouldPlay) { + return result } } } @@ -2602,6 +2638,8 @@ private func fontSizeForEmojiString(_ string: String) -> CGFloat { let basicSize: CGFloat = 94.0 let multiplier: CGFloat switch length { + case 1: + multiplier = 1.0 case 2: multiplier = 0.7 case 3: