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/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index 2ac8624611..d679668719 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -492,7 +492,10 @@ class PremiumStarComponent: Component { } } - let from = node.presentation.eulerAngles + var from = node.presentation.eulerAngles + if abs(from.y - .pi * 2.0) < 0.001 { + from.y = 0.0 + } node.removeAnimation(forKey: "tapRotate") var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 @@ -505,9 +508,9 @@ class PremiumStarComponent: Component { let to = SCNVector3(x: 0.0, y: toValue, z: 0.0) let distance = rad2deg(to.y - from.y) - guard !distance.isZero else { - return - } +// guard !distance.isZero else { +// return +// } let springAnimation = CASpringAnimation(keyPath: "eulerAngles") springAnimation.fromValue = NSValue(scnVector3: from) @@ -517,7 +520,11 @@ class PremiumStarComponent: Component { springAnimation.damping = 5.8 springAnimation.duration = springAnimation.settlingDuration * 0.75 springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 - + springAnimation.completion = { [weak node] finished in + if finished { + node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0) + } + } node.addAnimation(springAnimation, forKey: "rotate") } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 78899afafc..23dc215f07 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -411,7 +411,7 @@ private func stringForSelectiveSettings(strings: PresentationStrings, settings: } } -private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, accessChallengeData: PostboxAccessChallengeData, blockedPeerCount: Int?, activeWebsitesCount: Int, hasTwoStepAuth: Bool?, twoStepAuthData: TwoStepVerificationAccessConfiguration?, canAutoarchive: Bool, isPremium: Bool) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, accessChallengeData: PostboxAccessChallengeData, blockedPeerCount: Int?, activeWebsitesCount: Int, hasTwoStepAuth: Bool?, twoStepAuthData: TwoStepVerificationAccessConfiguration?, canAutoarchive: Bool, isPremiumDisabled: Bool, isPremium: Bool) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] entries.append(.blockedPeers(presentationData.theme, presentationData.strings.Settings_BlockedUsers, blockedPeerCount == nil ? "" : (blockedPeerCount == 0 ? presentationData.strings.PrivacySettings_BlockedPeersEmpty : "\(blockedPeerCount!)"))) @@ -449,7 +449,9 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.presence))) entries.append(.profilePhotoPrivacy(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.profilePhoto))) entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceCalls))) - entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceMessages), !isPremium)) + if !isPremiumDisabled { + entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceMessages), !isPremium)) + } entries.append(.forwardPrivacy(presentationData.theme, presentationData.strings.Privacy_Forwards, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.forwards))) entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.groupInvitations))) @@ -458,7 +460,9 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading)) entries.append(.profilePhotoPrivacy(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto, presentationData.strings.Channel_NotificationLoading)) entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, presentationData.strings.Channel_NotificationLoading)) - entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, presentationData.strings.Channel_NotificationLoading, !isPremium)) + if !isPremiumDisabled { + entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, presentationData.strings.Channel_NotificationLoading, !isPremium)) + } entries.append(.forwardPrivacy(presentationData.theme, presentationData.strings.Privacy_Forwards, presentationData.strings.Channel_NotificationLoading)) entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) @@ -772,10 +776,14 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let hapticFeedback = HapticFeedback() hapticFeedback.impact() + var alreadyPresented = false presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip), elevatedLayout: false, animateInAsReplacement: false, action: { action in if action == .info { - let controller = PremiumIntroScreen(context: context, source: .settings) - pushControllerImpl?(controller, true) + if !alreadyPresented { + let controller = PremiumIntroScreen(context: context, source: .settings) + pushControllerImpl?(controller, true) + alreadyPresented = true + } return true } return false @@ -961,8 +969,9 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let isPremium = accountPeer?.isPremium ?? false + let isPremiumDisabled = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }).isPremiumDisabled - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, canAutoarchive: canAutoarchive, isPremium: isPremium), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index ef51277f23..bfb70557ab 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -139,8 +139,8 @@ func chatHistoryEntriesForView( contentTypeHint = .animatedEmoji } else if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0], (message.textEntitiesAttribute?.entities.isEmpty ?? true) { contentTypeHint = .animatedEmoji - } else if message.text.count < 10 && messageIsElligibleForLargeEmoji(message) { - contentTypeHint = .largeEmoji + } else if messageIsElligibleForLargeEmoji(message) { + contentTypeHint = .animatedEmoji } } @@ -218,10 +218,12 @@ func chatHistoryEntriesForView( var contentTypeHint: ChatMessageEntryContentType = .generic if presentationData.largeEmoji, topMessage.media.isEmpty { - if stickersEnabled && topMessage.text.count == 1, let _ = associatedData.animatedEmojiStickers[topMessage.text.basicEmoji.0] { + if stickersEnabled && messageIsElligibleForLargeCustomEmoji(topMessage) { + contentTypeHint = .animatedEmoji + } else if stickersEnabled && topMessage.text.count == 1, let _ = associatedData.animatedEmojiStickers[topMessage.text.basicEmoji.0] { + contentTypeHint = .animatedEmoji + } else if messageIsElligibleForLargeEmoji(topMessage) { contentTypeHint = .animatedEmoji - } else if topMessage.text.count < 10 && messageIsElligibleForLargeEmoji(topMessage) { - contentTypeHint = .largeEmoji } } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 3c6a0446e5..1807f4aa38 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -543,17 +543,10 @@ 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) { - emojiString = item.message.text - } + if messageIsElligibleForLargeCustomEmoji(item.message) || (item.message.text.count > 1 && messageIsElligibleForLargeEmoji(item.message)) { + emojiString = item.message.text } - if emojiFile == nil && emojiString == nil { emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file } @@ -566,12 +559,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 +666,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 +705,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 +942,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) @@ -961,9 +967,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { deliveryFailedInset += 24.0 } + if item.message.forwardInfo != nil || item.message.attributes.first(where: { $0 is ReplyMessageAttribute }) != nil { + tmpWidth -= 60.0 + } + tmpWidth -= deliveryFailedInset - let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) + let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) - 70.0 let font = Font.regular(fontSizeForEmojiString(item.message.text)) let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: .black, linkColor: .black, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) @@ -1060,7 +1070,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var needsReplyBackground = false var replyMarkup: ReplyMarkupMessageAttribute? - let availableContentWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + + let availableContentWidth = min(120.0, max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)) var ignoreForward = false if let forwardInfo = item.message.forwardInfo { @@ -1632,7 +1643,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } private func commitEnqueuedAnimations() { - guard let item = self.item, let file = self.emojiFile, !self.enqueuedAdditionalAnimations.isEmpty else { + guard let item = self.item, !self.enqueuedAdditionalAnimations.isEmpty else { + return + } + + var emojiFile = self.emojiFile + if emojiFile == nil { + emojiFile = item.message.associatedMedia.first?.value as? TelegramMediaFile + } + + guard let file = emojiFile else { return } @@ -1744,7 +1764,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 +1796,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 +1828,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 +1915,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 +2131,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 } } } @@ -2592,7 +2639,7 @@ private func fontSizeForEmojiString(_ string: String) -> CGFloat { var maxLineLength = 0 for line in lines { - maxLineLength = max(maxLineLength, line.count) + maxLineLength = max(maxLineLength, line.replacingOccurrences(of: " ", with: "").count) } let linesCount = lines.count @@ -2602,6 +2649,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: diff --git a/submodules/TelegramUI/Sources/EmojiResources.swift b/submodules/TelegramUI/Sources/EmojiResources.swift index b1480eddd3..7b961272ca 100644 --- a/submodules/TelegramUI/Sources/EmojiResources.swift +++ b/submodules/TelegramUI/Sources/EmojiResources.swift @@ -192,7 +192,7 @@ private func matchingEmojiEntry(_ emoji: String) -> (UInt8, UInt8, UInt8)? { } func messageIsElligibleForLargeEmoji(_ message: Message) -> Bool { - if !message.text.isEmpty && message.text.containsOnlyEmoji && message.text.emojis.count < 6 { + if !message.text.isEmpty && message.text.containsOnlyEmoji { if !(message.textEntitiesAttribute?.entities.isEmpty ?? true) { return false }