diff --git a/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift b/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift index 7240457859..12ea9262ec 100644 --- a/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift +++ b/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift @@ -445,7 +445,7 @@ public class AnimatedCountLabelView: UIView { effectiveSegmentWidth = max(effectiveSegmentWidth, 4.0) } calculatedSegments[segment.key] = (layout, effectiveSegmentWidth, apply) - contentSize.width += effectiveSegmentWidth + contentSize.width += floor(effectiveSegmentWidth * 0.9) contentSize.height = max(contentSize.height, layout.size.height) remainingSize.width = max(0.0, remainingSize.width - layout.size.width) if layout.truncated { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index a59105ca76..8baa9978ff 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1273,6 +1273,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, dismissUrlPreview: { + }, dismissForwardMessages: { + }, dismissSuggestPost: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 394b9e6fc9..5761e92de2 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -82,9 +82,9 @@ public final class ChatPanelInterfaceInteraction { public let forwardCurrentForwardMessages: () -> Void public let forwardMessages: ([Message]) -> Void public let updateForwardOptionsState: ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void - public let presentForwardOptions: (ASDisplayNode) -> Void - public let presentReplyOptions: (ASDisplayNode) -> Void - public let presentLinkOptions: (ASDisplayNode) -> Void + public let presentForwardOptions: (UIView) -> Void + public let presentReplyOptions: (UIView) -> Void + public let presentLinkOptions: (UIView) -> Void public let presentSuggestPostOptions: () -> Void public let shareSelectedMessages: () -> Void public let updateTextInputStateAndMode: (@escaping (ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void @@ -187,6 +187,9 @@ public final class ChatPanelInterfaceInteraction { public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void public let dismissAllTooltips: () -> Void public let editTodoMessage: (MessageId, Int32?, Bool) -> Void + public let dismissUrlPreview: () -> Void + public let dismissForwardMessages: () -> Void + public let dismissSuggestPost: () -> Void public let requestLayout: (ContainedViewLayoutTransition) -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -205,9 +208,9 @@ public final class ChatPanelInterfaceInteraction { forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, updateForwardOptionsState: @escaping ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void, - presentForwardOptions: @escaping (ASDisplayNode) -> Void, - presentReplyOptions: @escaping (ASDisplayNode) -> Void, - presentLinkOptions: @escaping (ASDisplayNode) -> Void, + presentForwardOptions: @escaping (UIView) -> Void, + presentReplyOptions: @escaping (UIView) -> Void, + presentLinkOptions: @escaping (UIView) -> Void, presentSuggestPostOptions: @escaping () -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, @@ -306,6 +309,9 @@ public final class ChatPanelInterfaceInteraction { updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void, dismissAllTooltips: @escaping () -> Void, editTodoMessage: @escaping (MessageId, Int32?, Bool) -> Void, + dismissUrlPreview: @escaping () -> Void, + dismissForwardMessages: @escaping () -> Void, + dismissSuggestPost: @escaping () -> Void, updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, toggleChatSidebarMode: @escaping () -> Void, @@ -428,6 +434,9 @@ public final class ChatPanelInterfaceInteraction { self.updateRecordingTrimRange = updateRecordingTrimRange self.dismissAllTooltips = dismissAllTooltips self.editTodoMessage = editTodoMessage + self.dismissUrlPreview = dismissUrlPreview + self.dismissForwardMessages = dismissForwardMessages + self.dismissSuggestPost = dismissSuggestPost self.updateHistoryFilter = updateHistoryFilter self.updateChatLocationThread = updateChatLocationThread self.toggleChatSidebarMode = toggleChatSidebarMode @@ -559,6 +568,9 @@ public final class ChatPanelInterfaceInteraction { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, dismissUrlPreview: { + }, dismissForwardMessages: { + }, dismissSuggestPost: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 6c40e16d12..7ab83c663f 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1304,4 +1304,20 @@ public struct ComponentTransition { completion: completion ) } + + public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + if let blurFilter = CALayer.blur() { + blurFilter.setValue(toRadius as NSNumber, forKey: "inputRadius") + layer.filters = [blurFilter] + layer.animate(from: fromRadius as NSNumber, to: toRadius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: removeOnCompletion, completion: { [weak layer] flag in + if let layer { + if toRadius <= 0.0 { + layer.filters = nil + } + } + + completion?(flag) + }) + } + } } diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 7a3caa1c4e..92098851a4 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -78,6 +78,7 @@ public final class RoundedRectangle: Component { context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke)) } } + self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius)) UIGraphicsEndImageContext() } else if component.colors.count > 1 { diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 32392fb45d..3cb91fded3 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -146,6 +146,7 @@ public final class NavigationBackgroundNode: ASDisplayNode { private var enableBlur: Bool private var enableSaturation: Bool + private var customBlurRadius: CGFloat? public var effectView: UIVisualEffectView? private let backgroundNode: ASDisplayNode @@ -164,10 +165,11 @@ public final class NavigationBackgroundNode: ASDisplayNode { } } - public init(color: UIColor, enableBlur: Bool = true, enableSaturation: Bool = true) { + public init(color: UIColor, enableBlur: Bool = true, enableSaturation: Bool = true, customBlurRadius: CGFloat? = nil) { self._color = .clear self.enableBlur = enableBlur self.enableSaturation = enableSaturation + self.customBlurRadius = customBlurRadius self.backgroundNode = ASDisplayNode() @@ -222,6 +224,9 @@ public final class NavigationBackgroundNode: ASDisplayNode { if !allowedKeys.contains(filterName) { return false } + if let customBlurRadius = self.customBlurRadius, filterName == "gaussianBlur" { + filter.setValue(customBlurRadius as NSNumber, forKey: "inputRadius") + } return true } } diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 51d3094bf3..1de180df61 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 213 + return 214 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index ed54165140..9e77b7e6cd 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -948,9 +948,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio panelControlDestructiveColor: UIColor(rgb: 0xff3b30), inputBackgroundColor: UIColor(rgb: 0xffffff), inputStrokeColor: UIColor(rgb: 0x000000, alpha: 0.1), - inputPlaceholderColor: UIColor(rgb: 0xbebec0), + inputPlaceholderColor: UIColor(rgb: 0x909090, alpha: 0.7), inputTextColor: UIColor(rgb: 0x000000), - inputControlColor: UIColor(rgb: 0x868D98), + inputControlColor: UIColor(rgb: 0x202020, alpha: 0.6), actionControlFillColor: defaultDayAccentColor, actionControlForegroundColor: UIColor(rgb: 0xffffff), primaryTextColor: UIColor(rgb: 0x000000), diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index b8b8335927..1f2724351e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -504,19 +504,19 @@ public struct PresentationResourcesChat { public static func chatInputPanelAttachmentButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelAttachmentButtonImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: theme.chat.inputPanel.panelControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputPanelEditAttachmentButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelEditAttachmentButtonImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/Replace"), color: theme.chat.inputPanel.panelControlAccentColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/Replace"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputPanelExpandButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelExpandButtonImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconExpandInput"), color: theme.chat.inputPanel.panelControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconExpandInput"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } @@ -534,53 +534,53 @@ public struct PresentationResourcesChat { public static func chatInputTextFieldStickersImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldStickersImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldInputButtonsImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldInputButtonsImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconInputButtons"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconInputButtons"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldCommandsImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldCommandsImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconCommands"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconCommands"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldSilentPostOnImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldSilentPostOnImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSilentPostOn"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSilentPostOn"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldSilentPostOffImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldSilentPostOffImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSilentPostOff"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSilentPostOff"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldSuggestPostImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldSuggestPostImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSuggestPost"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSuggestPost"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldKeyboardImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldKeyboardImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldTimerImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldTimerImage.rawValue, { theme in - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconTimer"), color: theme.chat.inputPanel.inputControlColor) { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconTimer"), color: .white) { return generateImage(CGSize(width: image.size.width, height: image.size.height + 1.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: image.size)) - }) + })?.withRenderingMode(.alwaysTemplate) } else { return nil } @@ -589,13 +589,13 @@ public struct PresentationResourcesChat { public static func chatInputTextFieldScheduleImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldScheduleImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSchedule"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSchedule"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } public static func chatInputTextFieldGiftImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldGiftImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconGift"), color: theme.chat.inputPanel.inputControlColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconGift"), color: .white)?.withRenderingMode(.alwaysTemplate) }) } @@ -616,7 +616,7 @@ public struct PresentationResourcesChat { return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.rootController.navigationBar.accentTextColor.cgColor) + context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(1.5) let position = CGPoint(x: 9.0 - 0.5, y: 23.0) @@ -624,7 +624,7 @@ public struct PresentationResourcesChat { context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0)) context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0)) context.strokePath() - }) + })?.withRenderingMode(.alwaysTemplate) }) } @@ -633,7 +633,7 @@ public struct PresentationResourcesChat { return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.rootController.navigationBar.accentTextColor.cgColor) + context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(1.5) context.translateBy(x: size.width * 0.5, y: size.height * 0.5) @@ -644,7 +644,7 @@ public struct PresentationResourcesChat { context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0)) context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0)) context.strokePath() - }) + })?.withRenderingMode(.alwaysTemplate) }) } @@ -653,10 +653,10 @@ public struct PresentationResourcesChat { return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/NavigateToMentions"), color: theme.rootController.navigationBar.accentTextColor), let cgImage = image.cgImage { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/NavigateToMentions"), color: UIColor.white), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) } - }) + })?.withRenderingMode(.alwaysTemplate) }) } @@ -665,10 +665,10 @@ public struct PresentationResourcesChat { return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.rootController.navigationBar.accentTextColor), let cgImage = image.cgImage { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: UIColor.white), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) } - }) + })?.withRenderingMode(.alwaysTemplate) }) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 2f5be92e7b..15b0974d52 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -486,6 +486,9 @@ swift_library( "//submodules/TelegramUI/Components/FaceScanScreen", "//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist", "//submodules/ContactsHelper", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel", + "//submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel/BUILD new file mode 100644 index 0000000000..12f204e27b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputAccessoryPanel", + module_name = "ChatInputAccessoryPanel", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel/Sources/ChatInputAccessoryPanel.swift b/submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel/Sources/ChatInputAccessoryPanel.swift new file mode 100644 index 0000000000..d6ac3cde01 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel/Sources/ChatInputAccessoryPanel.swift @@ -0,0 +1,44 @@ +import Foundation +import UIKit +import TelegramPresentationData +import TelegramUIPreferences +import GlassBackgroundComponent + +public final class ChatInputAccessoryPanelEnvironment: Equatable { + public let theme: PresentationTheme + public let strings: PresentationStrings + public let nameDisplayOrder: PresentationPersonNameOrder + public let dateTimeFormat: PresentationDateTimeFormat + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + nameDisplayOrder: PresentationPersonNameOrder, + dateTimeFormat: PresentationDateTimeFormat + ) { + self.theme = theme + self.strings = strings + self.nameDisplayOrder = nameDisplayOrder + self.dateTimeFormat = dateTimeFormat + } + + public static func ==(lhs: ChatInputAccessoryPanelEnvironment, rhs: ChatInputAccessoryPanelEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + return true + } +} + +public protocol ChatInputAccessoryPanelView: UIView { + var contentTintView: UIView { get } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/BUILD new file mode 100644 index 0000000000..8bf7cbea8a --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputMessageAccessoryPanel", + module_name = "ChatInputMessageAccessoryPanel", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/AccountContext", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel", + "//submodules/Display", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TelegramStringFormatting", + "//submodules/PhotoResources", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/CompositeTextNode", + "//submodules/ChatInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift new file mode 100644 index 0000000000..328ce00df3 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift @@ -0,0 +1,911 @@ +import Foundation +import UIKit +import TelegramPresentationData +import ChatInputAccessoryPanel +import AccountContext +import TelegramCore +import SwiftSignalKit +import ComponentFlow +import Display +import GlassBackgroundComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import TelegramStringFormatting +import PhotoResources +import TextFormat +import CompositeTextNode +import ChatInterfaceState + +private func generateCloseIcon() -> UIImage { + return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() + })!.withRenderingMode(.alwaysTemplate) +} + +private func textStringForForwardedMessage(_ message: EngineMessage, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) { + for media in message.media { + switch media { + case _ as TelegramMediaImage: + return (strings.Message_Photo, [], true) + case let file as TelegramMediaFile: + if file.isVideoSticker || file.isAnimatedSticker { + return (strings.Message_Sticker, [], true) + } + var fileName: String = strings.Message_File + for attribute in file.attributes { + switch attribute { + case .Sticker: + return (strings.Message_Sticker, [], true) + case let .FileName(name): + fileName = name + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return (strings.Message_Audio, [], true) + } else { + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + return (title + " — " + performer, [], true) + } else if let title = title, !title.isEmpty { + return (title, [], true) + } else if let performer = performer, !performer.isEmpty { + return (performer, [], true) + } else { + return (strings.Message_Audio, [], true) + } + } + case .Video: + if file.isAnimated { + return (strings.Message_Animation, [], true) + } else { + return (strings.Message_Video, [], true) + } + default: + break + } + } + return (fileName, [], true) + case _ as TelegramMediaContact: + return (strings.Message_Contact, [], true) + case let game as TelegramMediaGame: + return (game.title, [], true) + case _ as TelegramMediaMap: + return (strings.Message_Location, [], true) + case _ as TelegramMediaAction: + return ("", [], true) + case _ as TelegramMediaPoll: + return (strings.ForwardedPolls(1), [], true) + case let todo as TelegramMediaTodo: + return (todo.text, [], true) + case let dice as TelegramMediaDice: + return (dice.emoji, [], true) + case let invoice as TelegramMediaInvoice: + return (invoice.title, [], true) + default: + break + } + } + return (message.text, message._asMessage().textEntitiesAttribute?.entities ?? [], false) +} + +public final class ChatInputMessageAccessoryPanel: Component { + public typealias EnvironmentType = ChatInputAccessoryPanelEnvironment + + public enum Contents: Equatable { + public final class Reply: Equatable { + public let id: EngineMessage.Id + public let quote: EngineMessageReplyQuote? + public let todoItemId: Int32? + public let message: EngineMessage? + + public init(id: EngineMessage.Id, quote: EngineMessageReplyQuote?, todoItemId: Int32?, message: EngineMessage?) { + self.id = id + self.quote = quote + self.todoItemId = todoItemId + self.message = message + } + + public static func ==(lhs: Reply, rhs: Reply) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.quote != rhs.quote { + return false + } + if lhs.todoItemId != rhs.todoItemId { + return false + } + if lhs.message?.id != rhs.message?.id { + return false + } + if lhs.message?.stableVersion != rhs.message?.stableVersion { + return false + } + return true + } + } + + public final class Edit: Equatable { + public let id: EngineMessage.Id + public let message: EngineMessage? + + public init(id: EngineMessage.Id, message: EngineMessage?) { + self.id = id + self.message = message + } + + public static func ==(lhs: Edit, rhs: Edit) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.message?.id != rhs.message?.id { + return false + } + if lhs.message?.stableVersion != rhs.message?.stableVersion { + return false + } + return true + } + } + + public final class Forward: Equatable { + public let messageIds: [EngineMessage.Id] + public let forwardOptionsState: ChatInterfaceForwardOptionsState? + + public init(messageIds: [EngineMessage.Id], forwardOptionsState: ChatInterfaceForwardOptionsState?) { + self.messageIds = messageIds + self.forwardOptionsState = forwardOptionsState + } + + public static func ==(lhs: Forward, rhs: Forward) -> Bool { + if lhs.messageIds != rhs.messageIds { + return false + } + if lhs.forwardOptionsState != rhs.forwardOptionsState { + return false + } + return true + } + } + + public final class LinkPreview: Equatable { + public let url: String + public let webpage: TelegramMediaWebpage + + public init(url: String, webpage: TelegramMediaWebpage) { + self.url = url + self.webpage = webpage + } + + public static func ==(lhs: LinkPreview, rhs: LinkPreview) -> Bool { + if lhs.url != rhs.url { + return false + } + if lhs.webpage != rhs.webpage { + return false + } + return true + } + } + + public final class SuggestPost: Equatable { + public let state: ChatInterfaceState.PostSuggestionState + + public init(state: ChatInterfaceState.PostSuggestionState) { + self.state = state + } + + public static func ==(lhs: SuggestPost, rhs: SuggestPost) -> Bool { + if lhs.state != rhs.state { + return false + } + return true + } + } + + case reply(Reply) + case edit(Edit) + case forward(Forward) + case linkPreview(LinkPreview) + case suggestPost(SuggestPost) + } + + let context: AccountContext + let contents: Contents + let chatPeerId: EnginePeer.Id? + let action: ((UIView) -> Void)? + let dismiss: (UIView) -> Void + + public init( + context: AccountContext, + contents: Contents, + chatPeerId: EnginePeer.Id?, + action: ((UIView) -> Void)?, + dismiss: @escaping (UIView) -> Void + ) { + self.context = context + self.contents = contents + self.chatPeerId = chatPeerId + self.action = action + self.dismiss = dismiss + } + + public static func ==(lhs: ChatInputMessageAccessoryPanel, rhs: ChatInputMessageAccessoryPanel) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.contents != rhs.contents { + return false + } + if lhs.chatPeerId != rhs.chatPeerId { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + public final class View: UIView, ChatInputAccessoryPanelView { + private let closeButton: HighlightTrackingButton + private let closeButtonIcon: GlassBackgroundView.ContentImageView + + private let lineView: UIImageView + private let titleNode: CompositeTextNode + private let text = ComponentView() + private let tintText = ComponentView() + + public let contentTintView: UIView + + private var isUpdating: Bool = false + private var component: ChatInputMessageAccessoryPanel? + private weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var messages: [EngineMessage] = [] + private var contentDisposable: Disposable? + + private var inlineTextStarImage: UIImage? + private var inlineTextTonImage: (UIImage, UIColor)? + + override public init(frame: CGRect) { + self.contentTintView = UIView() + + self.closeButton = HighlightTrackingButton() + self.closeButtonIcon = GlassBackgroundView.ContentImageView() + + self.lineView = UIImageView() + self.titleNode = CompositeTextNode() + + super.init(frame: frame) + + self.addSubview(self.lineView) + self.addSubview(self.titleNode.view) + + self.addSubview(self.closeButtonIcon) + self.contentTintView.addSubview(self.closeButtonIcon.tintMask) + self.addSubview(self.closeButton) + + self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), for: .touchUpInside) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.contentDisposable?.dispose() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + component.action?(self) + } + } + + @objc private func closeButtonPressed() { + guard let component = self.component else { + return + } + component.dismiss(self) + } + + public func update(component: ChatInputMessageAccessoryPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + + if self.component == nil { + let messageIds: [EngineMessage.Id] + switch component.contents { + case let .edit(edit): + messageIds = [edit.id] + case let .reply(reply): + messageIds = [reply.id] + case let .forward(forward): + messageIds = forward.messageIds + case .linkPreview, .suggestPost: + messageIds = [] + } + + self.contentDisposable?.dispose() + if !messageIds.isEmpty { + self.contentDisposable = (component.context.engine.data.subscribe( + EngineDataList(messageIds.map { id in + return TelegramEngine.EngineData.Item.Messages.Message(id: id) + }) + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] messages in + guard let self else { + return + } + self.messages = messages.compactMap { $0 } + if !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + }) + } + } + + self.component = component + self.state = state + self.environment = environment + + if self.closeButtonIcon.image == nil { + self.closeButtonIcon.image = generateCloseIcon() + } + if self.lineView.image == nil { + self.lineView.image = generateImage(CGSize(width: 2.0, height: 3.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 1.0).cgPath) + context.fillPath() + })?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: 1) + } + + let size = CGSize(width: availableSize.width, height: 52.0) + + let containerInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 6.0, right: 0.0) + + let lineSize = CGSize(width: 2.0, height: size.height - containerInsets.top - containerInsets.bottom) + let lineFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: lineSize) + transition.setFrame(view: self.lineView, frame: lineFrame) + self.lineView.tintColor = environment.theme.chat.inputPanel.panelControlAccentColor + + let closeButtonSize = CGSize(width: 44.0, height: 44.0) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width, y: floor((size.height - closeButtonSize.height) * 0.5)), size: closeButtonSize) + transition.setFrame(view: self.closeButton, frame: closeButtonFrame) + + if let image = self.closeButtonIcon.image { + let closeButtonIconFrame = image.size.centered(in: closeButtonFrame) + transition.setFrame(view: self.closeButtonIcon, frame: closeButtonIconFrame) + } + self.closeButtonIcon.tintColor = environment.theme.chat.inputPanel.inputControlColor + + let secondaryTextColor = environment.theme.chat.inputPanel.inputControlColor.withMultipliedBrightnessBy(0.5) + + var textString: NSAttributedString + var isPhoto = false + if self.messages.count == 1, let message = self.messages.first { + var text = "" + let effectiveMessage = message + //TODO:release media + /*if let currentEditMediaReference = self.currentEditMediaReference { + effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) + }*/ + let (attributedText, _, _) = descriptionStringForMessage( + contentSettings: component.context.currentContentSettings.with { $0 }, + message: effectiveMessage, + strings: environment.strings, + nameDisplayOrder: environment.nameDisplayOrder, + dateTimeFormat: environment.dateTimeFormat, + accountPeerId: component.context.account.peerId + ) + text = attributedText.string + + var updatedMediaReference: AnyMediaReference? + var imageDimensions: CGSize? + if !message._asMessage().containsSecretMedia { + var candidateMediaReference: AnyMediaReference? + for media in message.media { + if media is TelegramMediaImage || media is TelegramMediaFile { + candidateMediaReference = .message(message: MessageReference(message._asMessage()), media: media) + break + } + } + + if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) { + updatedMediaReference = imageReference.abstract + if let representation = largestRepresentationForPhoto(imageReference.media) { + imageDimensions = representation.dimensions.cgSize + } + } else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) { + updatedMediaReference = fileReference.abstract + if !fileReference.media.isInstantVideo, let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker { + imageDimensions = representation.dimensions.cgSize + } + } + } + + /*let imageNodeLayout = self.imageNode.asyncLayout() + var applyImage: (() -> Void)? + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 35.0, height: 35.0) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + } + + var mediaUpdated = false + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = self.previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) + } else if (updatedMediaReference != nil) != (self.previousMediaReference != nil) { + mediaUpdated = true + } + self.previousMediaReference = updatedMediaReference*/ + + let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + let _ = updateImageSignal + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + updateImageSignal = chatMessagePhotoThumbnail(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) + isPhoto = true + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) + } + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + + let isMedia: Bool + let isText: Bool + /*if let currentEditMediaReference = self.currentEditMediaReference { + effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) + }*/ + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + switch messageContentKind(contentSettings: component.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: environment.strings, nameDisplayOrder: environment.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: component.context.account.peerId) { + case .text: + isMedia = false + isText = true + default: + isMedia = true + isText = false + } + + let textFont = Font.regular(14.0) + let messageText: NSAttributedString + if isText { + let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in + switch entity.type { + case .Spoiler, .CustomEmoji: + return true + default: + return false + } + } + let textColor = environment.theme.chat.inputPanel.primaryTextColor + if entities.count > 0 { + messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) + } else { + messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? secondaryTextColor : environment.theme.chat.inputPanel.primaryTextColor) + } + } else { + messageText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? secondaryTextColor : environment.theme.chat.inputPanel.primaryTextColor) + } + textString = messageText + } else { + textString = NSAttributedString() + } + + var titleText: [CompositeTextNode.Component] = [] + switch component.contents { + case .edit: + let canEditMedia: Bool + //TODO:release + /*if let message = self.message, !messageMediaEditingOptions(message: message).isEmpty { + canEditMedia = true + } else { + canEditMedia = false + }*/ + canEditMedia = !"".isEmpty + + let titleStringValue: String + if let message = self.messages.first, message.id.namespace == Namespaces.Message.QuickReplyCloud { + titleStringValue = environment.strings.Conversation_EditingQuickReplyPanelTitle + } else if canEditMedia { + titleStringValue = isPhoto ? environment.strings.Conversation_EditingPhotoPanelTitle : environment.strings.Conversation_EditingCaptionPanelTitle + } else { + titleStringValue = environment.strings.Conversation_EditingMessagePanelTitle + } + titleText = [.text(NSAttributedString(string: titleStringValue, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + case let .reply(reply): + if let peer = self.messages.first?.peers[reply.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + let icon: UIImage? + icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon"), color: environment.theme.chat.inputPanel.panelControlAccentColor) + + if let icon { + let rawString: PresentationStrings.FormattedString + if reply.quote != nil { + rawString = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(peer.debugDisplayTitle) + } else { + rawString = environment.strings.Chat_ReplyPanel_ReplyTo(peer.debugDisplayTitle) + } + if let nameRange = rawString.ranges.first { + titleText = [] + + let rawNsString = rawString.string as NSString + if nameRange.range.lowerBound != 0 { + titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: 0, length: nameRange.range.lowerBound)), font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + } + titleText.append(.icon(icon)) + titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + + if nameRange.range.upperBound != rawNsString.length { + titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: nameRange.range.upperBound, length: rawNsString.length - nameRange.range.upperBound)), font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + } + } else { + titleText.append(.text(NSAttributedString(string: rawString.string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + } + } + } else { + var authorName = "" + if let forwardInfo = self.messages.first?._asMessage().forwardInfo, forwardInfo.flags.contains(.isImported) { + if let author = forwardInfo.author { + authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder) + } else if let authorSignature = forwardInfo.authorSignature { + authorName = authorSignature + } + } else if let author = self.messages.first?._asMessage().effectiveAuthor { + authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder) + } + + if let _ = reply.todoItemId { + let string = environment.strings.Chat_ReplyPanel_ReplyToTodoItem + titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + } else if let _ = reply.quote { + let string = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(authorName).string + titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + } else { + let string = environment.strings.Conversation_ReplyMessagePanelTitle(authorName).string + titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + } + + if reply.id.peerId != component.chatPeerId { + if let peer = self.messages.first?.peers[reply.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) { + let icon: UIImage? + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon") + } else { + icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon") + } + if let iconImage = generateTintedImage(image: icon, color: environment.theme.chat.inputPanel.panelControlAccentColor) { + titleText.append(.icon(iconImage)) + titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + } + } + } + + if let message = self.messages.first { + let textFont = Font.regular(14.0) + if let quote = reply.quote { + let textColor = environment.theme.chat.inputPanel.primaryTextColor + textString = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) + } else if let todoItemId = reply.todoItemId, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { + let textColor = environment.theme.chat.inputPanel.primaryTextColor + textString = stringWithAppliedEntities(trimToLineCount(todoItem.text, lineCount: 1), entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) + } + } + } + case let .forward(forward): + var title = "" + var authors = "" + var uniquePeerIds = Set() + var text = NSMutableAttributedString(string: "") + + for message in self.messages { + if let author = message.forwardInfo?.author ?? message._asMessage().effectiveAuthor, !uniquePeerIds.contains(author.id) { + uniquePeerIds.insert(author.id) + if !authors.isEmpty { + authors.append(", ") + } + if author.id == component.context.account.peerId { + authors.append(environment.strings.DialogList_You) + } else { + authors.append(EnginePeer(author).compactDisplayTitle) + } + } + } + + if self.messages.count == 1 { + title = environment.strings.Conversation_ForwardOptions_ForwardTitleSingle + let (string, entities, _) = textStringForForwardedMessage(messages[0], strings: environment.strings) + + text = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(authors): ", font: Font.regular(14.0), textColor: secondaryTextColor)) + + let additionalText = NSMutableAttributedString(attributedString: NSAttributedString(string: string, font: Font.regular(14.0), textColor: secondaryTextColor)) + for entity in entities { + switch entity.type { + case let .CustomEmoji(_, fileId): + let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + if range.lowerBound >= 0 && range.upperBound <= additionalText.length { + additionalText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: messages[0].associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile), range: range) + } + default: + break + } + } + + text.append(additionalText) + } else { + title = environment.strings.Conversation_ForwardOptions_ForwardTitle(Int32(messages.count)) + text = NSMutableAttributedString(attributedString: NSAttributedString(string: environment.strings.Conversation_ForwardFrom(authors).string, font: Font.regular(14.0), textColor: secondaryTextColor)) + } + + if forward.forwardOptionsState?.hideNames == true { + text = NSMutableAttributedString(attributedString: NSAttributedString(string: environment.strings.Conversation_ForwardOptions_SenderNamesRemoved, font: Font.regular(14.0), textColor: secondaryTextColor)) + } + + titleText = [.text(NSAttributedString(string: title, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + textString = text + case let .linkPreview(linkPreview): + var authorName = "" + var text = "" + switch linkPreview.webpage.content { + case .Pending: + authorName = environment.strings.Channel_NotificationLoading + text = linkPreview.url + case let .Loaded(content): + if let contentText = content.text { + text = contentText + } else { + if let file = content.file, let mediaKind = mediaContentKind(EngineMedia(file)) { + if content.type == "telegram_background" { + text = environment.strings.Message_Wallpaper + } else if content.type == "telegram_theme" { + text = environment.strings.Message_Theme + } else { + text = stringForMediaKind(mediaKind, strings: environment.strings).0.string + } + } else if content.type == "telegram_theme" { + text = environment.strings.Message_Theme + } else if content.type == "video" { + text = stringForMediaKind(.video, strings: environment.strings).0.string + } else if content.type == "telegram_story" { + text = stringForMediaKind(.story, strings: environment.strings).0.string + } else if let _ = content.image { + text = stringForMediaKind(.image, strings: environment.strings).0.string + } + } + + if let title = content.title { + authorName = title + } else if let websiteName = content.websiteName { + authorName = websiteName + } else { + authorName = content.displayUrl + } + } + + titleText = [.text(NSAttributedString(string: authorName, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + textString = NSAttributedString(string: text, font: Font.regular(14.0), textColor: environment.theme.chat.inputPanel.primaryTextColor) + case let .suggestPost(suggestPost): + if suggestPost.state.editingOriginalMessageId != nil { + titleText.append(.text(NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputEditTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + } else { + titleText.append(.text(NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))) + } + + let textFont = Font.regular(14.0) + + if let price = suggestPost.state.price, price.amount != .zero { + let currencySymbol: String + let amountString: String + switch price.currency { + case .stars: + currencySymbol = "#" + amountString = "\(price.amount)" + case .ton: + currencySymbol = "$" + amountString = formatTonAmountText(price.amount.value, dateTimeFormat: environment.dateTimeFormat) + } + if let timestamp = suggestPost.state.timestamp { + let timeString = humanReadableStringForTimestamp(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, timestamp: timestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat( + dateFormatString: { value in + return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_Date(value).string, ranges: []) + }, + tomorrowFormatString: { value in + return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TomorrowAt(value).string, ranges: []) + }, + todayFormatString: { value in + return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: []) + }, + yesterdayFormatString: { value in + return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: []) + } + )).string + textString = NSAttributedString(string: "\(currencySymbol)\(amountString) 📅 \(timeString)", font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor) + } else { + textString = NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputSubtitleAnytime("\(currencySymbol)\(amountString)").string, font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor) + } + } else { + textString = NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputSubtitleEmpty, font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor) + } + + let mutableTextString = NSMutableAttributedString(attributedString: textString) + for currency in [.stars, .ton] as [CurrencyAmount.Currency] { + var inlineTextStarImage: UIImage? + if let current = self.inlineTextStarImage { + inlineTextStarImage = current + } else { + if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") { + let starInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + inlineTextStarImage = generateImage(CGSize(width: starInsets.left + image.size.width + starInsets.right, height: image.size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + image.draw(at: CGPoint(x: starInsets.left, y: starInsets.top)) + })?.withRenderingMode(.alwaysOriginal) + self.inlineTextStarImage = inlineTextStarImage + } + } + + var inlineTextTonImage: UIImage? + if let current = self.inlineTextTonImage, current.1 == environment.theme.list.itemAccentColor { + inlineTextTonImage = current.0 + } else { + if let image = UIImage(bundleImageName: "Ads/TonMedium") { + let tonInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + let inlineTextTonImageValue = generateTintedImage(image: generateImage(CGSize(width: tonInsets.left + image.size.width + tonInsets.right, height: image.size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + image.draw(at: CGPoint(x: tonInsets.left, y: tonInsets.top)) + }), color: environment.theme.list.itemAccentColor)!.withRenderingMode(.alwaysOriginal) + inlineTextTonImage = inlineTextTonImageValue + self.inlineTextTonImage = (inlineTextTonImageValue, environment.theme.list.itemAccentColor) + } + } + + let currencySymbol: String + let currencyImage: UIImage? + switch currency { + case .stars: + currencySymbol = "#" + currencyImage = inlineTextStarImage + case .ton: + currencySymbol = "$" + currencyImage = inlineTextTonImage + } + + if let range = mutableTextString.string.range(of: currencySymbol), let currencyImage { + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + + let runDelegateData = RunDelegateData( + ascent: Font.regular(14.0).ascender, + descent: Font.regular(14.0).descender, + width: currencyImage.size.width + 2.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + mutableTextString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: mutableTextString.string)) + } + mutableTextString.addAttribute(.attachment, value: currencyImage, range: NSRange(range, in: mutableTextString.string)) + mutableTextString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: mutableTextString.string)) + mutableTextString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: mutableTextString.string)) + } + + textString = mutableTextString + } + } + + let textInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 0.0, right: 44.0) + + self.titleNode.components = titleText + let titleSize = self.titleNode.update(constrainedSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0)) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(textString), + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0) + ) + let tintTextString = NSMutableAttributedString(attributedString: textString) + tintTextString.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(location: 0, length: tintTextString.length)) + let _ = self.tintText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(tintTextString), + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0) + ) + + let titleTextSpacing: CGFloat = 1.0 + + let titleFrame = CGRect(origin: CGPoint(x: lineFrame.maxX + textInsets.left, y: textInsets.top), size: titleSize) + let textFrame = CGRect(origin: CGPoint(x: lineFrame.maxX + textInsets.left, y: titleFrame.maxY + titleTextSpacing), size: textSize) + + transition.setFrame(view: self.titleNode.view, frame: titleFrame) + + if let textView = self.text.view, let tintTextView = self.tintText.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + self.addSubview(textView) + + tintTextView.layer.anchorPoint = CGPoint() + self.contentTintView.addSubview(tintTextView) + } + transition.setPosition(view: textView, position: textFrame.origin) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + + transition.setPosition(view: tintTextView, position: textFrame.origin) + tintTextView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD index 57e1434665..bc482b0ebb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD @@ -16,6 +16,7 @@ swift_library( "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/ChatControllerInteraction", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift index 3c44da17c2..146451859d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift @@ -6,6 +6,7 @@ import Postbox import TelegramCore import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction public protocol ChatInputPanelViewForOverlayContent: UIView { func maybeDismissContent(point: CGPoint) @@ -13,6 +14,7 @@ public protocol ChatInputPanelViewForOverlayContent: UIView { open class ChatInputPanelNode: ASDisplayNode { open var context: AccountContext? + open var chatControllerInteraction: ChatControllerInteraction? open var interfaceInteraction: ChatPanelInterfaceInteraction? open var prevInputPanelNode: ChatInputPanelNode? diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index d267817ca8..20e7678405 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -173,6 +173,9 @@ public final class ChatRecentActionsController: TelegramBaseController { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, dismissUrlPreview: { + }, dismissForwardMessages: { + }, dismissSuggestPost: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift index ddcd0145ac..a63706f259 100644 --- a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift @@ -388,7 +388,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { let alertController = richTextAlertController(context: self.context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in if let strongSelf = self { - strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf) + strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf.view) Queue.mainQueue().after(0.5) { strongSelf.updateThemeAndStrings(theme: strongSelf.theme, strings: strongSelf.strings, forwardOptionsState: strongSelf.forwardOptionsState, force: true) } @@ -409,7 +409,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { return } self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() - self.interfaceInteraction?.presentForwardOptions(self) + self.interfaceInteraction?.presentForwardOptions(self.view) Queue.mainQueue().after(1.5) { self.updateThemeAndStrings(theme: self.theme, strings: self.strings, forwardOptionsState: self.forwardOptionsState, force: true) } diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 56d329e3be..9170830f35 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -492,7 +492,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { return } self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() - self.interfaceInteraction?.presentReplyOptions(self) + self.interfaceInteraction?.presentReplyOptions(self.view) Queue.mainQueue().after(1.5) { self.updateThemeAndStrings(theme: self.theme, strings: self.strings, force: true) } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 2413db2468..72a31c25c4 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -407,7 +407,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - fileprivate var clipContentToTopPanel: Bool = false + public var clipContentToTopPanel: Bool = false public var externalTopPanelContainerImpl: PagerExternalTopPanelContainer? public override var externalTopPanelContainer: UIView? { diff --git a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift index cfc7a2d504..2d263109cb 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift @@ -197,6 +197,13 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM private var modeTimeoutTimer: SwiftSignalKit.Timer? private let animationView: ComponentView + public var animationOutput: UIImageView? { + didSet { + if let view = self.animationView.view as? LottieComponent.View { + view.output = self.animationOutput + } + } + } private var recordingOverlay: ChatTextInputAudioRecordingOverlay? private var startTouchLocation: CGPoint? @@ -415,7 +422,7 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: animationName), - color: self.useDarkTheme ? .white : self.theme.chat.inputPanel.panelControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0) + color: self.useDarkTheme ? .white : self.theme.chat.inputPanel.inputControlColor )), environment: {}, containerSize: animationFrame.size @@ -425,6 +432,7 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM view.isUserInteractionEnabled = false if view.superview == nil { self.insertSubview(view, at: 0) + view.output = self.animationOutput self.updateShadow() } view.frame = animationFrame @@ -433,6 +441,10 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM view.playOnce() } } + + if let animationOutput = self.animationOutput { + animationOutput.frame = animationFrame + } } public func updateTheme(theme: PresentationTheme) { @@ -553,6 +565,11 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM if let layer = self.animationView.view?.layer { transition.updateAlpha(layer: layer, alpha: 0.0) transition.updateTransformScale(layer: layer, scale: 0.3) + + if let animationOutput = self.animationOutput { + transition.updateAlpha(layer: animationOutput.layer, alpha: 0.0) + transition.updateTransformScale(layer: animationOutput.layer, scale: 0.3) + } } } @@ -569,6 +586,11 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM if let layer = self.animationView.view?.layer { transition.updateAlpha(layer: layer, alpha: 1.0) transition.updateTransformScale(layer: layer, scale: 1.0) + + if let animationOutput = self.animationOutput { + transition.updateAlpha(layer: animationOutput.layer, alpha: 1.0) + transition.updateTransformScale(layer: animationOutput.layer, scale: 1.0) + } } } } @@ -582,6 +604,11 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM let iconSize = view.bounds.size view.bounds = CGRect(origin: .zero, size: iconSize) view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + if let animationOutput = self.animationOutput { + animationOutput.bounds = view.bounds + animationOutput.center = view.center + } } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index fd0198c52d..1935765c7e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -732,6 +732,7 @@ public final class EntityKeyboardComponent: Component { topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( theme: component.theme, overflowHeight: component.hiddenInputHeight, + topInset: 12.0, displayBackground: component.externalTopPanelContainer != nil ? .none : component.displayTopPanelBackground )), externalTopPanelContainer: component.externalTopPanelContainer, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index a8e4fae508..c0f3538db9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -38,15 +38,18 @@ final class EntityKeyboardTopContainerPanelComponent: Component { let theme: PresentationTheme let overflowHeight: CGFloat + let topInset: CGFloat let displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground init( theme: PresentationTheme, overflowHeight: CGFloat, + topInset: CGFloat, displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground ) { self.theme = theme self.overflowHeight = overflowHeight + self.topInset = topInset self.displayBackground = displayBackground } @@ -57,6 +60,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component { if lhs.overflowHeight != rhs.overflowHeight { return false } + if lhs.topInset != rhs.topInset { + return false + } if lhs.displayBackground != rhs.displayBackground { return false } @@ -95,7 +101,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let intrinsicHeight: CGFloat = 34.0 - let height = intrinsicHeight + let height = intrinsicHeight + component.topInset let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value @@ -122,7 +128,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { let panel = panelEnvironment.contentTopPanels[index] let indexOffset = index - centralIndex - let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: -component.overflowHeight), size: CGSize(width: availableSize.width, height: intrinsicHeight + component.overflowHeight)) + let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: component.topInset - component.overflowHeight), size: CGSize(width: availableSize.width, height: intrinsicHeight + component.overflowHeight)) let isInBounds = visibleBounds.intersects(panelFrame) let isPartOfTransition: Bool diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/BUILD b/submodules/TelegramUI/Components/GlassBackgroundComponent/BUILD new file mode 100644 index 0000000000..941836106e --- /dev/null +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GlassBackgroundComponent", + module_name = "GlassBackgroundComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift new file mode 100644 index 0000000000..910841c6bb --- /dev/null +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -0,0 +1,517 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ComponentDisplayAdapters + +private func generateForegroundImage(size: CGSize, isDark: Bool, fillColor: UIColor) -> UIImage { + var size = size + if size == .zero { + size = CGSize(width: 1.0, height: 1.0) + } + + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let maxColor = UIColor(white: 1.0, alpha: isDark ? 0.67 : 0.9) + let minColor = UIColor(white: 1.0, alpha: 0.0) + + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + let lineWidth: CGFloat = isDark ? 0.66 : 0.66 + + context.saveGState() + + let darkShadeColor = UIColor(white: isDark ? 1.0 : 0.0, alpha: 0.035) + let lightShadeColor = UIColor(white: isDark ? 0.0 : 1.0, alpha: 0.035) + let innerShadowBlur: CGFloat = 24.0 + + context.resetClip() + context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context.clip() + context.addRect(CGRect(origin: CGPoint(), size: size).insetBy(dx: -100.0, dy: -100.0)) + context.addEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + context.setShadow(offset: CGSize(width: 10.0, height: -10.0), blur: innerShadowBlur, color: darkShadeColor.cgColor) + context.fillPath(using: .evenOdd) + + context.resetClip() + context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context.clip() + context.addRect(CGRect(origin: CGPoint(), size: size).insetBy(dx: -100.0, dy: -100.0)) + context.addEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + context.setShadow(offset: CGSize(width: -10.0, height: 10.0), blur: innerShadowBlur, color: lightShadeColor.cgColor) + context.fillPath(using: .evenOdd) + + context.restoreGState() + + context.setLineWidth(lineWidth) + + context.addRect(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))) + context.clip() + context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context.replacePathWithStrokedPath() + context.clip() + + do { + var locations: [CGFloat] = [0.0, 0.5, 0.5 + 0.2, 1.0 - 0.1, 1.0] + let colors: [CGColor] = [maxColor.cgColor, maxColor.cgColor, minColor.cgColor, minColor.cgColor, maxColor.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + + context.resetClip() + context.addRect(CGRect(origin: CGPoint(x: size.width - size.width * 0.5, y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))) + context.clip() + context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context.replacePathWithStrokedPath() + context.clip() + + do { + var locations: [CGFloat] = [0.0, 0.1, 0.5 - 0.2, 0.5, 1.0] + let colors: [CGColor] = [maxColor.cgColor, minColor.cgColor, minColor.cgColor, maxColor.cgColor, maxColor.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + })!.stretchableImage(withLeftCapWidth: Int(size.width * 0.5), topCapHeight: Int(size.height * 0.5)) +} + +private final class ContentContainer: UIView { + private let maskContentView: UIView + + init(maskContentView: UIView) { + self.maskContentView = maskContentView + + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result === self { + return nil + } + return result + } + + override func didAddSubview(_ subview: UIView) { + super.didAddSubview(subview) + + if let subview = subview as? GlassBackgroundView.ContentView { + self.maskContentView.addSubview(subview.tintMask) + } + } + + override func willRemoveSubview(_ subview: UIView) { + super.willRemoveSubview(subview) + + if let subview = subview as? GlassBackgroundView.ContentView { + subview.tintMask.removeFromSuperview() + } + } +} + +public final class GlassBackgroundView: UIView { + public protocol ContentView: UIView { + var tintMask: UIView { get } + } + + open class ContentLayer: SimpleLayer { + public var targetLayer: CALayer? + + override init() { + super.init() + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public var position: CGPoint { + get { + return super.position + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.position = value + } + super.position = value + } + } + + override public var bounds: CGRect { + get { + return super.bounds + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.bounds = value + } + super.bounds = value + } + } + + override public var anchorPoint: CGPoint { + get { + return super.anchorPoint + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.anchorPoint = value + } + super.anchorPoint = value + } + } + + override public var anchorPointZ: CGFloat { + get { + return super.anchorPointZ + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.anchorPointZ = value + } + super.anchorPointZ = value + } + } + + override public var opacity: Float { + get { + return super.opacity + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.opacity = value + } + super.opacity = value + } + } + + override public var sublayerTransform: CATransform3D { + get { + return super.sublayerTransform + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.sublayerTransform = value + } + super.sublayerTransform = value + } + } + + override public var transform: CATransform3D { + get { + return super.transform + } set(value) { + if let targetLayer = self.targetLayer { + targetLayer.transform = value + } + super.transform = value + } + } + + override public func add(_ animation: CAAnimation, forKey key: String?) { + if let targetLayer = self.targetLayer { + targetLayer.add(animation, forKey: key) + } + + super.add(animation, forKey: key) + } + + override public func removeAllAnimations() { + if let targetLayer = self.targetLayer { + targetLayer.removeAllAnimations() + } + + super.removeAllAnimations() + } + + override public func removeAnimation(forKey: String) { + if let targetLayer = self.targetLayer { + targetLayer.removeAnimation(forKey: forKey) + } + + super.removeAnimation(forKey: forKey) + } + } + + public final class ContentColorView: UIView, ContentView { + override public static var layerClass: AnyClass { + return ContentLayer.self + } + + public let tintMask: UIView + + override public init(frame: CGRect) { + self.tintMask = UIView() + + super.init(frame: CGRect()) + + self.tintMask.tintColor = .black + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + public final class ContentImageView: UIImageView, ContentView { + override public static var layerClass: AnyClass { + return ContentLayer.self + } + + private let tintImageView: UIImageView + public var tintMask: UIView { + return self.tintImageView + } + + override public var image: UIImage? { + didSet { + self.tintImageView.image = self.image + } + } + + override public init(frame: CGRect) { + self.tintImageView = UIImageView() + + super.init(frame: CGRect()) + + self.tintImageView.tintColor = .black + } + + override public init(image: UIImage?) { + self.tintImageView = UIImageView() + + super.init(image: image) + + self.tintImageView.image = image + self.tintImageView.tintColor = .black + } + + override public init(image: UIImage?, highlightedImage: UIImage?) { + self.tintImageView = UIImageView() + + super.init(image: image, highlightedImage: highlightedImage) + + self.tintImageView.image = image + self.tintImageView.tintColor = .black + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private struct Params: Equatable { + let cornerRadius: CGFloat + let isDark: Bool + let tintColor: UIColor + + init(cornerRadius: CGFloat, isDark: Bool, tintColor: UIColor) { + self.cornerRadius = cornerRadius + self.isDark = isDark + self.tintColor = tintColor + } + } + + private let backgroundNode: NavigationBackgroundNode? + private let nativeView: UIVisualEffectView? + + private let foregroundView: UIImageView? + + public let maskContentView: UIView + private let contentContainer: ContentContainer + + public var contentView: UIView { + return self.contentContainer + } + + private var params: Params? + + public override init(frame: CGRect) { + if #available(iOS 26.0, *) { + self.backgroundNode = nil + let glassEffect = UIGlassEffect(style: .clear) + glassEffect.isInteractive = false + let nativeView = UIVisualEffectView(effect: glassEffect) + self.nativeView = nativeView + //nativeView.overrideUserInterfaceStyle = .light + //nativeView.traitOverrides.userInterfaceStyle = .light + self.foregroundView = UIImageView() + //self.foregroundView = nil + } else { + self.backgroundNode = NavigationBackgroundNode(color: .black, enableBlur: true, customBlurRadius: 5.0) + self.nativeView = nil + self.foregroundView = UIImageView() + } + + self.maskContentView = UIView() + self.maskContentView.backgroundColor = .white + if let filter = CALayer.luminanceToAlpha() { + self.maskContentView.layer.filters = [filter] + } + + self.contentContainer = ContentContainer(maskContentView: self.maskContentView) + + super.init(frame: frame) + + if let nativeView = self.nativeView { + self.addSubview(nativeView) + } + if let backgroundNode = self.backgroundNode { + self.addSubview(backgroundNode.view) + } + if let foregroundView = self.foregroundView { + self.addSubview(foregroundView) + foregroundView.mask = self.maskContentView + } + self.addSubview(self.contentContainer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: UIColor, transition: ComponentTransition) { + if let nativeView = self.nativeView { + let previousFrame = nativeView.frame + + transition.containedViewLayoutTransition.animateView { + nativeView.layer.cornerRadius = cornerRadius + nativeView.frame = CGRect(origin: CGPoint(), size: size) + } + + nativeView.layer.animateFrame(from: previousFrame, to: CGRect(origin: CGPoint(), size: size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + if let backgroundNode = self.backgroundNode { + backgroundNode.updateColor(color: .clear, forceKeepBlur: tintColor.alpha != 1.0, transition: transition.containedViewLayoutTransition) + backgroundNode.update(size: size, cornerRadius: cornerRadius, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + } + + let params = Params(cornerRadius: cornerRadius, isDark: isDark, tintColor: tintColor) + if self.params != params { + self.params = params + + /*if let nativeView { + if #available(iOS 26.0, *) { + let glassEffect = UIGlassEffect(style: .regular) + glassEffect.tintColor = tintColor + glassEffect.isInteractive = false + + nativeView.effect = glassEffect + } + }*/ + + if let foregroundView = self.foregroundView { + foregroundView.image = generateForegroundImage(size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0), isDark: isDark, fillColor: tintColor) + } + } + + transition.setFrame(view: self.maskContentView, frame: CGRect(origin: CGPoint(), size: size)) + if let foregroundView { + transition.setFrame(view: foregroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: size)) + } +} + +public final class VariableBlurView: UIVisualEffectView { + public let maxBlurRadius: CGFloat + + public var gradientMask: UIImage { + didSet { + if self.gradientMask !== oldValue { + self.resetEffect() + } + } + } + + public init(gradientMask: UIImage, maxBlurRadius: CGFloat = 20.0) { + self.gradientMask = gradientMask + self.maxBlurRadius = maxBlurRadius + + super.init(effect: UIBlurEffect(style: .regular)) + + self.resetEffect() + + if self.subviews.indices.contains(1) { + let tintOverlayView = subviews[1] + tintOverlayView.alpha = 0 + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + self.resetEffect() + } + } + + private func resetEffect() { + let filterClassStringEncoded = "Q0FGaWx0ZXI=" + let filterClassString: String = { + if + let data = Data(base64Encoded: filterClassStringEncoded), + let string = String(data: data, encoding: .utf8) + { + return string + } + + return "" + }() + let filterWithTypeStringEncoded = "ZmlsdGVyV2l0aFR5cGU6" + let filterWithTypeString: String = { + if + let data = Data(base64Encoded: filterWithTypeStringEncoded), + let string = String(data: data, encoding: .utf8) + { + return string + } + + return "" + }() + + let filterWithTypeSelector = Selector(filterWithTypeString) + + guard let filterClass = NSClassFromString(filterClassString) as AnyObject as? NSObjectProtocol else { + return + } + + guard filterClass.responds(to: filterWithTypeSelector) else { + return + } + + let variableBlur = filterClass.perform(filterWithTypeSelector, with: "variableBlur").takeUnretainedValue() + + guard let variableBlur = variableBlur as? NSObject else { + return + } + + guard let gradientImageRef = self.gradientMask.cgImage else { + return + } + + variableBlur.setValue(self.maxBlurRadius, forKey: "inputRadius") + variableBlur.setValue(gradientImageRef, forKey: "inputMaskImage") + variableBlur.setValue(true, forKey: "inputNormalizeEdges") + variableBlur.setValue(UIScreenScale, forKey: "scale") + + let backdropLayer = self.subviews.first?.layer + backdropLayer?.filters = [variableBlur] + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 21c72e8719..c23d6bf347 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -439,6 +439,9 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, dismissUrlPreview: { + }, dismissForwardMessages: { + }, dismissSuggestPost: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index a151b5416f..b5380ef304 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -582,7 +582,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return items } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceView: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in if let selectedMessageIds = chatController?.selectedMessageIds { var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -826,6 +826,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, dismissUrlPreview: { + }, dismissForwardMessages: { + }, dismissSuggestPost: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { @@ -1819,7 +1822,7 @@ private func stringForRequestPeerType(strings: PresentationStrings, peerType: Re private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController - weak var sourceNode: ASDisplayNode? + weak var sourceView: UIView? let sourceRect: CGRect? let navigationController: NavigationController? = nil @@ -1828,17 +1831,24 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceNode?.view + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceView = sourceView self.sourceRect = sourceRect self.passthroughTouches = passthroughTouches } func transitionInfo() -> ContextControllerTakeControllerInfo? { - let sourceNode = self.sourceNode + let sourceView = self.sourceView let sourceRect = self.sourceRect - return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in - if let sourceNode = sourceNode { - return (sourceNode.view, sourceRect ?? sourceNode.bounds) + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in + if let sourceView { + return (sourceView, sourceRect ?? sourceView.bounds) } else { return nil } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index a0ea346e17..833a4d8dde 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1982,21 +1982,21 @@ extension ChatControllerImpl { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState(f($0.forwardOptionsState ?? ChatInterfaceForwardOptionsState(hideNames: false, hideCaptions: false, unhideNamesOnCaptionChange: false))) }) }) } - }, presentForwardOptions: { [weak self] sourceNode in + }, presentForwardOptions: { [weak self] sourceView in guard let self else { return } - presentChatForwardOptions(selfController: self, sourceNode: sourceNode) - }, presentReplyOptions: { [weak self] sourceNode in + presentChatForwardOptions(selfController: self, sourceView: sourceView) + }, presentReplyOptions: { [weak self] sourceView in guard let self else { return } - presentChatReplyOptions(selfController: self, sourceNode: sourceNode) - }, presentLinkOptions: { [weak self] sourceNode in + presentChatReplyOptions(selfController: self, sourceView: sourceView) + }, presentLinkOptions: { [weak self] sourceView in guard let self else { return } - presentChatLinkOptions(selfController: self, sourceNode: sourceNode) + presentChatLinkOptions(selfController: self, sourceView: sourceView) }, presentSuggestPostOptions: { [weak self] in guard let self else { return @@ -4340,6 +4340,30 @@ extension ChatControllerImpl { return } self.openTodoEditing(messageId: messageId, itemId: itemId, append: append) + }, dismissUrlPreview: { [weak self] in + guard let self else { + return + } + self.chatDisplayNode.dismissUrlPreview() + }, dismissForwardMessages: { [weak self] in + guard let self else { + return + } + self.chatDisplayNode.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil) }) + }, dismissSuggestPost: { [weak self] in + guard let self else { + return + } + self.chatDisplayNode.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { state in + var state = state + if let postSuggestionState = state.postSuggestionState { + state = state.withUpdatedPostSuggestionState(nil) + if postSuggestionState.editingOriginalMessageId != nil { + state = state.withUpdatedEditMessage(nil) + } + } + return state + }) }, updateHistoryFilter: { [weak self] update in guard let self else { return diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index e81ab0238b..20457a3e21 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -27,28 +27,28 @@ private enum OptionsId: Hashable { case link } -private func presentChatInputOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, initialId: OptionsId) { +private func presentChatInputOptions(selfController: ChatControllerImpl, sourceView: UIView, initialId: OptionsId) { var getContextController: (() -> ContextController?)? var sources: [ContextController.Source] = [] let replySelectionState = Promise(ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: false, quote: nil)) - if let source = chatReplyOptions(selfController: selfController, sourceNode: sourceNode, getContextController: { + if let source = chatReplyOptions(selfController: selfController, sourceView: sourceView, getContextController: { return getContextController?() }, selectionState: replySelectionState) { sources.append(source) } var forwardDismissedForCancel: (() -> Void)? - if let (source, dismissedForCancel) = chatForwardOptions(selfController: selfController, sourceNode: sourceNode, getContextController: { + if let (source, dismissedForCancel) = chatForwardOptions(selfController: selfController, sourceView: sourceView, getContextController: { return getContextController?() }) { forwardDismissedForCancel = dismissedForCancel sources.append(source) } - if let source = chatLinkOptions(selfController: selfController, sourceNode: sourceNode, getContextController: { + if let source = chatLinkOptions(selfController: selfController, sourceView: sourceView, getContextController: { return getContextController?() }, replySelectionState: replySelectionState) { sources.append(source) @@ -84,7 +84,7 @@ private func presentChatInputOptions(selfController: ChatControllerImpl, sourceN selfController.presentInGlobalOverlay(contextController) } -private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?) -> (ContextController.Source, () -> Void)? { +private func chatForwardOptions(selfController: ChatControllerImpl, sourceView: UIView, getContextController: @escaping () -> ContextController?) -> (ContextController.Source, () -> Void)? { guard let peerId = selfController.chatLocation.peerId else { return nil } @@ -270,13 +270,13 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: return (ContextController.Source( id: AnyHashable(OptionsId.forward), title: selfController.presentationData.strings.Conversation_MessageOptionsTabForward, - source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceView: sourceView, passthroughTouches: true)), items: items |> map { ContextController.Items(id: AnyHashable("forward"), content: .list($0)) } ), dismissedForCancel) } -func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) { - presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .forward) +func presentChatForwardOptions(selfController: ChatControllerImpl, sourceView: UIView) { + presentChatInputOptions(selfController: selfController, sourceView: sourceView, initialId: .forward) } private func generateChatReplyOptionItems(selfController: ChatControllerImpl, chatController: ChatControllerImpl) -> Signal { @@ -498,7 +498,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch return items } -private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, selectionState: Promise) -> ContextController.Source? { +private func chatReplyOptions(selfController: ChatControllerImpl, sourceView: UIView, getContextController: @escaping () -> ContextController?, selectionState: Promise) -> ContextController.Source? { guard let peerId = selfController.chatLocation.peerId else { return nil } @@ -543,13 +543,13 @@ private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: AS return ContextController.Source( id: AnyHashable(OptionsId.reply), title: selfController.presentationData.strings.Conversation_MessageOptionsTabReply, - source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceView: sourceView, passthroughTouches: true)), items: items ) } -func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) { - presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .reply) +func presentChatReplyOptions(selfController: ChatControllerImpl, sourceView: UIView) { + presentChatInputOptions(selfController: selfController, sourceView: sourceView, initialId: .reply) } func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) { @@ -716,7 +716,7 @@ func moveReplyToChat(selfController: ChatControllerImpl, peerId: EnginePeer.Id, }) } -private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, replySelectionState: Promise) -> ContextController.Source? { +private func chatLinkOptions(selfController: ChatControllerImpl, sourceView: UIView, getContextController: @escaping () -> ContextController?, replySelectionState: Promise) -> ContextController.Source? { guard let peerId = selfController.chatLocation.peerId else { return nil } @@ -969,13 +969,13 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD return ContextController.Source( id: AnyHashable(OptionsId.link), title: selfController.presentationData.strings.Conversation_MessageOptionsTabLink, - source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceView: sourceView, passthroughTouches: true)), items: items ) } -func presentChatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) { - presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .link) +func presentChatLinkOptions(selfController: ChatControllerImpl, sourceView: UIView) { + presentChatInputOptions(selfController: selfController, sourceView: sourceView, initialId: .link) } extension ChatControllerImpl { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index bcd4b9a5a6..3a8e778e19 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -46,6 +46,7 @@ import ComponentFlow import ChatEmptyNode import SpaceWarpView import ChatSideTopicsPanel +import GlassBackgroundComponent final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -214,12 +215,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let inputPanelBackgroundNode: NavigationBackgroundNode private var navigationBarBackgroundContent: WallpaperBubbleBackgroundNode? - private var inputPanelBackgroundContent: WallpaperBubbleBackgroundNode? private var intrinsicInputPanelBackgroundNodeSize: CGSize? - private let inputPanelBackgroundSeparatorNode: ASDisplayNode private var inputPanelBottomBackgroundSeparatorBaseOffset: CGFloat = 0.0 - private let inputPanelBottomBackgroundSeparatorNode: ASDisplayNode private var plainInputSeparatorAlpha: CGFloat? private var usePlainInputSeparator: Bool @@ -237,6 +235,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var leftPanelContainer: ChatControllerTitlePanelNodeContainer private(set) var leftPanel: (component: AnyComponentWithIdentity, view: ComponentView)? + private var inputPanelBackgroundBlurMask: UIImageView? + private var inputPanelBackgroundBlurView: VariableBlurView? + private(set) var inputPanelNode: ChatInputPanelNode? private(set) var inputPanelOverscrollNode: ChatInputPanelOverscrollNode? private weak var currentDismissedInputPanelNode: ChatInputPanelNode? @@ -736,22 +737,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelClippingNode = SparseNode() if case let .color(color) = self.chatPresentationInterfaceState.chatWallpaper, UIColor(rgb: color).isEqual(self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { - self.inputPanelBackgroundNode = NavigationBackgroundNode(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) + self.inputPanelBackgroundNode = NavigationBackgroundNode(color: .clear) self.usePlainInputSeparator = true } else { - self.inputPanelBackgroundNode = NavigationBackgroundNode(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor) + self.inputPanelBackgroundNode = NavigationBackgroundNode(color: .clear) self.usePlainInputSeparator = false self.plainInputSeparatorAlpha = nil } - //self.inputPanelBackgroundNode.isUserInteractionEnabled = false - - self.inputPanelBackgroundSeparatorNode = ASDisplayNode() - self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor - self.inputPanelBackgroundSeparatorNode.isLayerBacked = true - - self.inputPanelBottomBackgroundSeparatorNode = ASDisplayNode() - self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor - self.inputPanelBottomBackgroundSeparatorNode.isLayerBacked = true self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated) self.navigateButtons.accessibilityElementsHidden = true @@ -851,8 +843,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) self.inputPanelContainerNode.addSubnode(self.inputPanelOverlayNode) self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode) - self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode) - self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) self.wrappingNode.contentNode.addSubnode(self.messageTransitionNode) self.contentContainerNode.contentNode.addSubnode(self.navigateButtons) @@ -1622,7 +1612,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var dismissedInputContextPanelNode: ChatInputContextPanelNode? var dismissedOverlayContextPanelNode: ChatInputContextPanelNode? - let inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) + let inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, chatControllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction) let inputPanelBottomInset = max(insets.bottom, inputPanelBottomInsetTerm) @@ -1751,15 +1741,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } var dismissedInputNode: ChatInputNode? - var dismissedInputNodeInputBackgroundExtension: CGFloat = 0.0 var dismissedInputNodeExternalTopPanelContainer: UIView? var immediatelyLayoutInputNodeAndAnimateAppearance = false var inputNodeHeightAndOverflow: (CGFloat, CGFloat)? if let inputNode = inputNodeForState { if self.inputNode != inputNode { - inputNode.topBackgroundExtensionUpdated = { [weak self] transition in - self?.updateInputPanelBackgroundExtension(transition: transition) - } inputNode.hideInputUpdated = { [weak self] transition in guard let strongSelf = self else { return @@ -1774,11 +1760,14 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } } + if let inputNode = inputNode as? ChatEntityKeyboardInputNode { + inputNode.externalTopPanelContainerImpl = nil + } + inputNode.clipsToBounds = true + inputNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + inputNode.layer.cornerRadius = 30.0 dismissedInputNode = self.inputNode - if let inputNode = self.inputNode { - dismissedInputNodeInputBackgroundExtension = inputNode.topBackgroundExtension - } dismissedInputNodeExternalTopPanelContainer = self.inputNode?.externalTopPanelContainer self.inputNode = inputNode inputNode.alpha = 1.0 @@ -1812,7 +1801,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { ) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode - dismissedInputNodeInputBackgroundExtension = inputNode.topBackgroundExtension dismissedInputNodeExternalTopPanelContainer = inputNode.externalTopPanelContainer self.inputNode = nil } @@ -2142,6 +2130,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.overlayContextPanelNode = nil } + let inputPanelsInset: CGFloat = 8.0 + let accessoryPanelsInset: CGFloat = 8.0 var inputPanelsHeight: CGFloat = 0.0 var inputPanelFrame: CGRect? @@ -2158,7 +2148,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if self.inputPanelNode != nil { - inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) + inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelsInset - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) inputPanelFrame = inputPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelHideOffset) if self.dismissedAsOverlay { inputPanelFrame!.origin.y = layout.size.height @@ -2181,7 +2171,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var accessoryPanelFrame: CGRect? if self.accessoryPanelNode != nil { assert(accessoryPanelSize != nil) - accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomOverflowOffset - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) + accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomOverflowOffset - insets.bottom - inputPanelsInset - inputPanelsHeight - accessoryPanelsInset - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) accessoryPanelFrame = accessoryPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelHideOffset) if self.dismissedAsOverlay { accessoryPanelFrame!.origin.y = layout.size.height @@ -2202,18 +2192,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - let inputBackgroundInset: CGFloat - if cleanInsets.bottom < insets.bottom { - if case .regular = layout.metrics.widthClass, insets.bottom < 88.0 { - inputBackgroundInset = insets.bottom - } else { - inputBackgroundInset = 0.0 - } - } else { - inputBackgroundInset = cleanInsets.bottom - } - - var inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight + inputBackgroundInset)) + var inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelsInset), size: CGSize(width: layout.size.width, height: inputPanelsHeight)) if self.dismissedAsOverlay { inputBackgroundFrame.origin.y = layout.size.height } @@ -2223,6 +2202,58 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + if !"".isEmpty { + let blurView: VariableBlurView + let blurMask: UIImageView + if let current = self.inputPanelBackgroundBlurMask { + blurMask = current + } else { + blurMask = UIImageView() + self.inputPanelBackgroundBlurMask = blurMask + + blurMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 16.0), colors: [UIColor(white: 1.0, alpha: 0.0), UIColor(white: 1.0, alpha: 0.0), UIColor(white: 1.0, alpha: 1.0)], locations: [0.0, 0.5, 1.0])?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 16) + } + + if let current = self.inputPanelBackgroundBlurView { + blurView = current + } else { + let baseGradientAlpha: CGFloat = 0.5 + let numSteps = 8 + let firstStep = 1 + let firstLocation = 0.5 + let colors = (0 ..< numSteps).map { i -> UIColor in + if i < firstStep { + return UIColor(white: 1.0, alpha: 1.0) + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return UIColor(white: 1.0, alpha: baseGradientAlpha * value) + } + } + let locations = (0 ..< numSteps).map { i -> CGFloat in + if i < firstStep { + return 0.0 + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) + } + } + + let backgroundBlurImage = generateGradientImage(size: CGSize(width: 8.0, height: 100.0), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })! + blurView = VariableBlurView(gradientMask: backgroundBlurImage, maxBlurRadius: 15.0) + self.inputPanelBackgroundBlurView = blurView + self.historyNodeContainer.view.superview?.insertSubview(blurView, aboveSubview: self.historyNodeContainer.view) + + blurView.mask = blurMask + } + + var blurFrame = inputBackgroundFrame + blurFrame.origin.y -= 18.0 + blurFrame.size.height += 100.0 + transition.updateFrame(view: blurView, frame: blurFrame) + transition.updateFrame(view: blurMask, frame: CGRect(origin: CGPoint(), size: blurFrame.size)) + } + let additionalScrollDistance: CGFloat = 0.0 var scrollToTop = false if dismissedInputByDragging { @@ -2233,7 +2264,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - var contentBottomInset: CGFloat = inputPanelsHeight + 4.0 + var contentBottomInset: CGFloat = inputPanelsHeight + 11.0 + inputPanelsInset if let scrollContainerNode = self.scrollContainerNode { transition.updateFrame(node: scrollContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -2428,7 +2459,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.historyNode.scrollEnabled = !self.isScrollingLockedAtTop let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) - var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) + var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 8.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 20.0), size: navigateButtonsSize) if case .overlay = self.chatPresentationInterfaceState.mode { navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0) } @@ -2533,45 +2564,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { navigationBarBackgroundContent.update(rect: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0) + (translationPanelHeight ?? 0.0))), within: layout.size, transition: transition) } - if let inputPanelBackgroundContent = self.inputPanelBackgroundContent { - var extensionValue: CGFloat = 0.0 - if let inputNode = self.inputNode { - extensionValue = inputNode.topBackgroundExtension - } - let apparentInputBackgroundFrame = CGRect(origin: apparentInputBackgroundFrame.origin, size: CGSize(width: apparentInputBackgroundFrame.width, height: apparentInputBackgroundFrame.height + extensionValue)) - var transition = transition - var delay: Double = 0.0 - if apparentInputBackgroundFrame.height > inputPanelBackgroundContent.frame.height { - transition = .immediate - } else if case let .animated(_, curve) = transition, case .spring = curve { - delay = 0.3 - } - - transition.updateFrame(node: inputPanelBackgroundContent, frame: CGRect(origin: .zero, size: apparentInputBackgroundFrame.size), beginWithCurrentState: true, delay: delay) - inputPanelBackgroundContent.update(rect: apparentInputBackgroundFrame, within: layout.size, delay: delay, transition: transition) - } - transition.updateFrame(node: self.contentDimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: apparentInputBackgroundFrame.origin.y))) let intrinsicInputPanelBackgroundNodeSize = CGSize(width: apparentInputBackgroundFrame.size.width, height: apparentInputBackgroundFrame.size.height) self.intrinsicInputPanelBackgroundNodeSize = intrinsicInputPanelBackgroundNodeSize - var inputPanelBackgroundExtension: CGFloat = 0.0 - if let inputNode = self.inputNode { - inputPanelBackgroundExtension = inputNode.topBackgroundExtension - } else { - inputPanelBackgroundExtension = dismissedInputNodeInputBackgroundExtension - } - var inputPanelUpdateTransition = transition - if immediatelyLayoutInputNodeAndAnimateAppearance { - inputPanelUpdateTransition = .immediate - } - - self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: inputPanelUpdateTransition, beginWithCurrentState: true) self.inputPanelBottomBackgroundSeparatorBaseOffset = intrinsicInputPanelBackgroundNodeSize.height - inputPanelUpdateTransition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: UIScreenPixel)), beginWithCurrentState: true) - transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) self.navigateButtons.update(rect: apparentNavigateButtonsFrame, within: layout.size, transition: transition) @@ -2703,10 +2702,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if immediatelyLayoutInputNodeAndAnimateAppearance { var adjustedForPreviousInputHeightFrame = inputNodeFrame var heightDifference = inputNodeHeight - previousInputHeight - var externalTopPanelContainerOffset: CGFloat = 0.0 if previousInputHeight.isLessThanOrEqualTo(cleanInsets.bottom) { - heightDifference = inputNodeHeight - inputPanelBackgroundExtension - externalTopPanelContainerOffset = inputPanelBackgroundExtension + heightDifference = inputNodeHeight } adjustedForPreviousInputHeightFrame.origin.y += heightDifference inputNode.frame = adjustedForPreviousInputHeightFrame @@ -2715,7 +2712,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { inputNode.updateAbsoluteRect(inputNodeFrame, within: layout.size, transition: transition) if let externalTopPanelContainer = inputNode.externalTopPanelContainer { - externalTopPanelContainer.frame = CGRect(origin: adjustedForPreviousInputHeightFrame.offsetBy(dx: 0.0, dy: externalTopPanelContainerOffset).origin, size: CGSize(width: adjustedForPreviousInputHeightFrame.width, height: 0.0)) + externalTopPanelContainer.frame = CGRect(origin: adjustedForPreviousInputHeightFrame.offsetBy(dx: 0.0, dy: 0.0).origin, size: CGSize(width: adjustedForPreviousInputHeightFrame.width, height: 0.0)) transition.updateFrame(view: externalTopPanelContainer, frame: CGRect(origin: inputNodeFrame.origin, size: CGSize(width: inputNodeFrame.width, height: 0.0))) } } else { @@ -3482,28 +3479,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { //self.notifyTransitionCompletionListeners(transition: transition) } - private func updateInputPanelBackgroundExtension(transition: ContainedViewLayoutTransition) { - guard let intrinsicInputPanelBackgroundNodeSize = self.intrinsicInputPanelBackgroundNodeSize else { - return - } - - var extensionValue: CGFloat = 0.0 - if let inputNode = self.inputNode { - extensionValue = inputNode.topBackgroundExtension - } - - self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + extensionValue), transition: transition) - transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBottomBackgroundSeparatorBaseOffset + extensionValue), size: CGSize(width: self.inputPanelBottomBackgroundSeparatorNode.bounds.width, height: UIScreenPixel)), beginWithCurrentState: true) - - if let inputPanelBackgroundContent = self.inputPanelBackgroundContent, let (layout, _) = self.validLayout { - var inputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame - inputPanelBackgroundFrame.size.height = intrinsicInputPanelBackgroundNodeSize.height + extensionValue - - transition.updateFrame(node: inputPanelBackgroundContent, frame: CGRect(origin: .zero, size: inputPanelBackgroundFrame.size)) - inputPanelBackgroundContent.update(rect: inputPanelBackgroundFrame, within: layout.size, transition: transition) - } - } - private var storedHideInputExpanded: Bool? private func updateInputPanelBackgroundExpansion(transition: ContainedViewLayoutTransition) { @@ -3601,43 +3576,30 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if themeUpdated { if case let .color(color) = self.chatPresentationInterfaceState.chatWallpaper, UIColor(rgb: color).isEqual(self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { - self.inputPanelBackgroundNode.updateColor(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper, transition: .immediate) self.usePlainInputSeparator = true } else { - self.inputPanelBackgroundNode.updateColor(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) self.usePlainInputSeparator = false self.plainInputSeparatorAlpha = nil } self.updatePlainInputSeparator(transition: .immediate) - self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor - self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor self.backgroundNode.updateBubbleTheme(bubbleTheme: chatPresentationInterfaceState.theme, bubbleCorners: chatPresentationInterfaceState.bubbleCorners) if self.backgroundNode.hasExtraBubbleBackground() { if self.navigationBarBackgroundContent == nil { - if let navigationBarBackgroundContent = self.backgroundNode.makeBubbleBackground(for: .free), - let inputPanelBackgroundContent = self.backgroundNode.makeBubbleBackground(for: .free) { + if let navigationBarBackgroundContent = self.backgroundNode.makeBubbleBackground(for: .free) { self.navigationBarBackgroundContent = navigationBarBackgroundContent - self.inputPanelBackgroundContent = inputPanelBackgroundContent navigationBarBackgroundContent.allowsGroupOpacity = true navigationBarBackgroundContent.implicitContentUpdate = false navigationBarBackgroundContent.alpha = 0.3 self.navigationBar?.insertSubnode(navigationBarBackgroundContent, at: 1) - - inputPanelBackgroundContent.allowsGroupOpacity = true - inputPanelBackgroundContent.implicitContentUpdate = false - inputPanelBackgroundContent.alpha = 0.3 - self.inputPanelBackgroundNode.addSubnode(inputPanelBackgroundContent) } } } else { self.navigationBarBackgroundContent?.removeFromSupernode() self.navigationBarBackgroundContent = nil - self.inputPanelBackgroundContent?.removeFromSupernode() - self.inputPanelBackgroundContent = nil } } @@ -4124,7 +4086,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let inputPanelsOffset = self.bounds.size.height - self.inputPanelBackgroundNode.frame.minY transition.animateFrame(node: self.inputPanelBackgroundNode, from: self.inputPanelBackgroundNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) - transition.animateFrame(node: self.inputPanelBackgroundSeparatorNode, from: self.inputPanelBackgroundSeparatorNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) if let inputPanelNode = self.inputPanelNode { transition.animateFrame(node: inputPanelNode, from: inputPanelNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) } @@ -4595,7 +4556,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return interfaceState.withUpdatedReplyMessageSubject(replyMessageSubject) } }) - presentChatLinkOptions(selfController: controller, sourceNode: controller.displayNode) + presentChatLinkOptions(selfController: controller, sourceView: controller.displayNode.view) }), ], parseMarkdown: true), in: .window(.root)) @@ -4865,10 +4826,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } resolvedValue = resolvedValue * (1.0 - self.inputPanelContainerNode.expansionFraction) - - if resolvedValue != self.inputPanelBackgroundSeparatorNode.alpha { - transition.updateAlpha(node: self.inputPanelBackgroundSeparatorNode, alpha: resolvedValue, beginWithCurrentState: true) - } } private var previousConfettiAnimationTimestamp: Double? diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index 9648d94310..1262582940 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -5,6 +5,9 @@ import Display import TelegramPresentationData import WallpaperBackgroundNode import AnimatedCountLabelNode +import GlassBackgroundComponent +import ComponentFlow +import ComponentDisplayAdapters private let badgeFont = Font.with(size: 13.0, traits: [.monospacedNumbers]) @@ -18,11 +21,9 @@ enum ChatHistoryNavigationButtonType { class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { let containerNode: ContextExtractedContentContainingNode let buttonNode: HighlightTrackingButtonNode - private let backgroundNode: NavigationBackgroundNode - private var backgroundContent: WallpaperBubbleBackgroundNode? - let backgroundImageNode: ASImageNode - let imageNode: ASImageNode - private let badgeBackgroundNode: ASImageNode + private let backgroundView: GlassBackgroundView + let imageView: GlassBackgroundView.ContentImageView + private let badgeBackgroundView: GlassBackgroundView private let badgeTextNode: ImmediateAnimatedCountLabelNode var tapped: (() -> Void)? { @@ -55,32 +56,22 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.containerNode = ContextExtractedContentContainingNode() self.buttonNode = HighlightTrackingButtonNode() - self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) + self.backgroundView = GlassBackgroundView() - self.backgroundImageNode = ASImageNode() - self.backgroundImageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonBackground(theme) - self.backgroundImageNode.isLayerBacked = true - - self.backgroundImageNode.displayWithoutProcessing = true - self.imageNode = ASImageNode() - self.imageNode.displayWithoutProcessing = true + self.imageView = GlassBackgroundView.ContentImageView() switch type { - case .down: - self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) - case .up: - self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) - case .mentions: - self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) - case .reactions: - self.imageNode.image = PresentationResourcesChat.chatHistoryReactionsButtonImage(theme) + case .down: + self.imageView.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .up: + self.imageView.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) + case .mentions: + self.imageView.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) + case .reactions: + self.imageView.image = PresentationResourcesChat.chatHistoryReactionsButtonImage(theme) } - self.imageNode.isLayerBacked = true - self.badgeBackgroundNode = ASImageNode() - self.badgeBackgroundNode.displayWithoutProcessing = true - self.badgeBackgroundNode.displaysAsynchronously = false - self.badgeBackgroundNode.image = PresentationResourcesChat.chatHistoryNavigationButtonBadgeImage(theme) - self.badgeBackgroundNode.alpha = 0.0 + self.badgeBackgroundView = GlassBackgroundView() + self.badgeBackgroundView.alpha = 0.0 self.badgeTextNode = ImmediateAnimatedCountLabelNode() self.badgeTextNode.isUserInteractionEnabled = false @@ -93,7 +84,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.addSubnode(self.containerNode) - let size = CGSize(width: 38.0, height: 38.0) + let size = CGSize(width: 40.0, height: 40.0) self.containerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) self.containerNode.contentRect = CGRect(origin: CGPoint(), size: size) @@ -101,17 +92,16 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) self.containerNode.contentNode.addSubnode(self.buttonNode) - self.buttonNode.addSubnode(self.backgroundNode) - self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: size.width / 2.0, transition: .immediate) + self.buttonNode.view.addSubview(self.backgroundView) + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: .immediate) + self.imageView.tintColor = theme.chat.inputPanel.inputControlColor - self.buttonNode.addSubnode(self.backgroundImageNode) - self.buttonNode.addSubnode(self.imageNode) - self.backgroundImageNode.frame = CGRect(origin: CGPoint(), size: size) - self.imageNode.frame = CGRect(origin: CGPoint(), size: size) + self.backgroundView.contentView.addSubview(self.imageView) + self.imageView.frame = CGRect(origin: CGPoint(), size: size) - self.buttonNode.addSubnode(self.badgeBackgroundNode) - self.badgeBackgroundNode.addSubnode(self.badgeTextNode) + self.buttonNode.view.addSubview(self.badgeBackgroundView) + self.badgeBackgroundView.contentView.addSubview(self.badgeTextNode.view) self.frame = CGRect(origin: CGPoint(), size: size) } @@ -120,19 +110,21 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { if self.theme !== theme { self.theme = theme - self.backgroundNode.updateColor(color: theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) + self.backgroundView.update(size: self.backgroundView.bounds.size, cornerRadius: self.backgroundView.bounds.size.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: .immediate) + self.imageView.tintColor = theme.chat.inputPanel.inputControlColor + switch self.type { - case .down: - self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) - case .up: - self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) - case .mentions: - self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) - case .reactions: - self.imageNode.image = PresentationResourcesChat.chatHistoryReactionsButtonImage(theme) + case .down: + self.imageView.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .up: + self.imageView.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) + case .mentions: + self.imageView.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) + case .reactions: + self.imageView.image = PresentationResourcesChat.chatHistoryReactionsButtonImage(theme) } - self.backgroundImageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonBackground(theme) - self.badgeBackgroundNode.image = PresentationResourcesChat.chatHistoryNavigationButtonBadgeImage(theme) + + self.badgeBackgroundView.update(size: self.badgeBackgroundView.bounds.size, cornerRadius: self.badgeBackgroundView.bounds.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.actionControlFillColor, transition: .immediate) var segments: [AnimatedCountLabelNode.Segment] = [] if let value = Int(self.badge) { @@ -144,34 +136,11 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { } self.badgeTextNode.segments = segments } - - if backgroundNode.hasExtraBubbleBackground() { - if self.backgroundContent == nil { - if let backgroundContent = backgroundNode.makeBubbleBackground(for: .free) { - backgroundContent.allowsGroupOpacity = true - backgroundContent.clipsToBounds = true - backgroundContent.alpha = 0.3 - backgroundContent.cornerRadius = 19.0 - backgroundContent.frame = self.backgroundNode.frame - self.buttonNode.insertSubnode(backgroundContent, aboveSubnode: self.backgroundNode) - self.backgroundContent = backgroundContent - } - } - } else { - self.backgroundContent?.removeFromSupernode() - self.backgroundContent = nil - } - - if let (rect, containerSize) = self.absoluteRect { - self.backgroundContent?.update(rect: rect, within: containerSize, transition: .immediate) - } } private var absoluteRect: (CGRect, CGSize)? func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { self.absoluteRect = (rect, containerSize) - - self.backgroundContent?.update(rect: rect, within: containerSize, transition: transition) } @objc func onTap() { @@ -195,43 +164,47 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.badgeTextNode.segments = segments let badgeSize = self.badgeTextNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true) - let backgroundSize = CGSize(width: self.badge.count == 1 ? 18.0 : max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) - let backgroundFrame = CGRect(origin: CGPoint(x: floor((38.0 - backgroundSize.width) / 2.0), y: -9.0), size: backgroundSize) - if backgroundFrame.width < self.badgeBackgroundNode.frame.width { - self.badgeBackgroundNode.layer.animateFrame(from: self.badgeBackgroundNode.frame, to: backgroundFrame, duration: 0.2) - self.badgeBackgroundNode.frame = backgroundFrame + let backgroundSize = CGSize(width: self.badge.count == 1 ? 20.0 : max(20.0, badgeSize.width + 10.0 + 1.0), height: 20.0) + let backgroundFrame = CGRect(origin: CGPoint(x: floor((40.0 - backgroundSize.width) / 2.0), y: -7.0), size: backgroundSize) + if backgroundFrame.width < self.badgeBackgroundView.frame.width { + self.badgeBackgroundView.layer.animateFrame(from: self.badgeBackgroundView.frame, to: backgroundFrame, duration: 0.2) + self.badgeBackgroundView.frame = backgroundFrame } else { - self.badgeBackgroundNode.frame = backgroundFrame + self.badgeBackgroundView.frame = backgroundFrame } - self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - badgeSize.width) / 2.0), y: 1.0), size: badgeSize) - if self.badgeBackgroundNode.alpha < 1.0 { - self.badgeBackgroundNode.alpha = 1.0 + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + self.badgeBackgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: self.theme.chat.inputPanel.actionControlFillColor, transition: ComponentTransition(transition)) + + self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - badgeSize.width) / 2.0), y: 2.0), size: badgeSize) + + if self.badgeBackgroundView.alpha < 1.0 { + self.badgeBackgroundView.alpha = 1.0 - self.badgeBackgroundNode.layer.animateScale(from: 0.01, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in + self.badgeBackgroundView.layer.animateScale(from: 0.01, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in if let strongSelf = self { - strongSelf.badgeBackgroundNode.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in - strongSelf.badgeBackgroundNode.layer.removeAllAnimations() + strongSelf.badgeBackgroundView.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in + strongSelf.badgeBackgroundView.layer.removeAllAnimations() }) } }) - self.badgeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.badgeBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } else if previousValue < self.currentValue { - self.badgeBackgroundNode.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in + self.badgeBackgroundView.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in if let strongSelf = self { - strongSelf.badgeBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in - strongSelf.badgeBackgroundNode.layer.removeAllAnimations() + strongSelf.badgeBackgroundView.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in + strongSelf.badgeBackgroundView.layer.removeAllAnimations() }) } }) } } else { self.currentValue = 0 - if self.badgeBackgroundNode.alpha > 0.0 { - self.badgeBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - self.badgeBackgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + if self.badgeBackgroundView.alpha > 0.0 { + self.badgeBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.badgeBackgroundView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) } - self.badgeBackgroundNode.alpha = 0.0 + self.badgeBackgroundView.alpha = 0.0 } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift index bf49d88053..190d7b524b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift @@ -175,14 +175,14 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } func updateLayout(transition: ContainedViewLayoutTransition) -> CGSize { - let buttonSize = CGSize(width: 38.0, height: 38.0) - let completeSize = CGSize(width: buttonSize.width, height: buttonSize.height * 2.0 + 12.0) + let buttonSize = CGSize(width: 40.0, height: 40.0) + let completeSize = CGSize(width: buttonSize.width, height: buttonSize.height * 2.0 + 8.0) var upOffset: CGFloat = 0.0 var mentionsOffset: CGFloat = 0.0 var reactionsOffset: CGFloat = 0.0 if let down = self.directionButtonState.down { - self.downButton.imageNode.alpha = down.isEnabled ? 1.0 : 0.5 + self.downButton.imageView.alpha = down.isEnabled ? 1.0 : 0.5 self.downButton.buttonNode.isEnabled = down.isEnabled mentionsOffset += buttonSize.height + 12.0 @@ -202,7 +202,7 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } if let up = self.directionButtonState.up { - self.upButton.imageNode.alpha = up.isEnabled ? 1.0 : 0.5 + self.upButton.imageView.alpha = up.isEnabled ? 1.0 : 0.5 self.upButton.buttonNode.isEnabled = up.isEnabled mentionsOffset += buttonSize.height + 12.0 diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index eee20d1fe2..7c213a885b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -9,8 +9,22 @@ import AccessoryPanelNode import ForwardAccessoryPanelNode import ReplyAccessoryPanelNode import SuggestPostAccessoryPanelNode +import ChatInputAccessoryPanel +import ChatInputMessageAccessoryPanel +import ComponentFlow +import TelegramNotices +import PresentationDataUtils +import Display +import Markdown +import TextFormat +import TelegramPresentationData -func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { +func textInputAccessoryPanel( + context: AccountContext, + chatPresentationInterfaceState: ChatPresentationInterfaceState, + chatControllerInteraction: ChatControllerInteraction?, + interfaceInteraction: ChatPanelInterfaceInteraction? +) -> AnyComponentWithIdentity? { if case .standard(.previewing) = chatPresentationInterfaceState.mode { return nil } @@ -30,27 +44,227 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage, chatPresentationInterfaceState.interfaceState.postSuggestionState == nil { if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, !editMessage.disableUrlPreviews.contains(editingUrlPreview.url) { - if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { - previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(url: editingUrlPreview.url, webpage: editingUrlPreview.webPage) - previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return previewPanelNode - } else { - let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.url, webpage: editingUrlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - panelNode.interfaceInteraction = interfaceInteraction - return panelNode - } + var previousTapTimestamp: Double? + return AnyComponentWithIdentity(id: "linkPreview", component: AnyComponent(ChatInputMessageAccessoryPanel( + context: context, + contents: .linkPreview(ChatInputMessageAccessoryPanel.Contents.LinkPreview( + url: editingUrlPreview.url, + webpage: editingUrlPreview.webPage + )), + chatPeerId: chatPresentationInterfaceState.chatLocation.peerId, + action: { sourceView in + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + previousTapTimestamp = CFAbsoluteTimeGetCurrent() + interfaceInteraction?.presentLinkOptions(sourceView) + }, + dismiss: { _ in + interfaceInteraction?.dismissUrlPreview() + } + ))) } - if let editPanelNode = currentPanel as? EditAccessoryPanelNode, editPanelNode.messageId == editMessage.messageId { - editPanelNode.interfaceInteraction = interfaceInteraction - editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return editPanelNode - } else { - let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) - panelNode.interfaceInteraction = interfaceInteraction - return panelNode + return AnyComponentWithIdentity(id: "edit", component: AnyComponent(ChatInputMessageAccessoryPanel( + context: context, + contents: .edit(ChatInputMessageAccessoryPanel.Contents.Edit( + id: editMessage.messageId, + message: nil + )), + chatPeerId: chatPresentationInterfaceState.chatLocation.peerId, + action: { _ in + }, + dismiss: { _ in + interfaceInteraction?.setupEditMessage(nil, { _ in }) + } + ))) + } else if let urlPreview = chatPresentationInterfaceState.urlPreview, !chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) { + var previousTapTimestamp: Double? + return AnyComponentWithIdentity(id: "linkPreview", component: AnyComponent(ChatInputMessageAccessoryPanel( + context: context, + contents: .linkPreview(ChatInputMessageAccessoryPanel.Contents.LinkPreview( + url: urlPreview.url, + webpage: urlPreview.webPage + )), + chatPeerId: chatPresentationInterfaceState.chatLocation.peerId, + action: { sourceView in + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + previousTapTimestamp = CFAbsoluteTimeGetCurrent() + interfaceInteraction?.presentLinkOptions(sourceView) + }, + dismiss: { _ in + interfaceInteraction?.dismissUrlPreview() + } + ))) + } else if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds { + var chatPeerId: EnginePeer.Id? + if let peerId = chatPresentationInterfaceState.chatLocation.peerId { + chatPeerId = peerId + } else if case .customChatContents = chatPresentationInterfaceState.chatLocation { + chatPeerId = context.account.peerId } + if let chatPeerId { + var previousTapTimestamp: Double? + let theme = chatPresentationInterfaceState.theme + let strings = chatPresentationInterfaceState.strings + let nameDisplayOrder = chatPresentationInterfaceState.nameDisplayOrder + let fontSize = chatPresentationInterfaceState.fontSize + + return AnyComponentWithIdentity(id: "forward", component: AnyComponent(ChatInputMessageAccessoryPanel( + context: context, + contents: .forward(ChatInputMessageAccessoryPanel.Contents.Forward( + messageIds: forwardMessageIds, + forwardOptionsState: chatPresentationInterfaceState.interfaceState.forwardOptionsState + )), + chatPeerId: chatPeerId, + action: { sourceView in + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + previousTapTimestamp = CFAbsoluteTimeGetCurrent() + interfaceInteraction?.presentForwardOptions(sourceView) + let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: context.sharedContext.accountManager, count: 3).start() + }, + dismiss: { sourceView in + Task { @MainActor [weak sourceView] in + guard let messageId = forwardMessageIds.first else { + return + } + guard let message = await context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.Message(id: messageId) + ).get() else { + return + } + guard let peer = message.peers[message.id.peerId] else { + return + } + + let peerId = peer.id + let peerDisplayTitle = EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder) + + let messageCount = Int32(forwardMessageIds.count) + let messages = strings.Conversation_ForwardOptions_Messages(messageCount) + let string: PresentationStrings.FormattedString + if peerId == context.account.peerId { + string = strings.Conversation_ForwardOptions_TextSaved(messages) + } else if peerId.namespace == Namespaces.Peer.CloudUser { + string = strings.Conversation_ForwardOptions_TextPersonal(messages, peerDisplayTitle) + } else { + string = strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle) + } + + let font = Font.regular(floor(fontSize.baseDisplaySize * 15.0 / 17.0)) + let boldFont = Font.semibold(floor(fontSize.baseDisplaySize * 15.0 / 17.0)) + let body = MarkdownAttributeSet(font: font, textColor: theme.actionSheet.secondaryTextColor) + let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.actionSheet.secondaryTextColor) + + let title = NSAttributedString(string: strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(fontSize.baseDisplaySize)), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center) + + let alertController = richTextAlertController(context: context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: strings.Conversation_ForwardOptions_ShowOptions, action: { + guard let sourceView else { + return + } + interfaceInteraction?.presentForwardOptions(sourceView) + let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: context.sharedContext.accountManager, count: 3).start() + }), TextAlertAction(type: .destructiveAction, title: strings.Conversation_ForwardOptions_CancelForwarding, action: { + interfaceInteraction?.dismissForwardMessages() + })], actionLayout: .vertical) + interfaceInteraction?.presentController(alertController, nil) + } + } + ))) + } else { + return nil + } + } else if let replyMessageSubject = chatPresentationInterfaceState.interfaceState.replyMessageSubject { + var chatPeerId: EnginePeer.Id? + if let peerId = chatPresentationInterfaceState.chatLocation.peerId { + chatPeerId = peerId + } else if case .customChatContents = chatPresentationInterfaceState.chatLocation { + chatPeerId = context.account.peerId + } + if let chatPeerId { + var previousTapTimestamp: Double? + return AnyComponentWithIdentity(id: "reply", component: AnyComponent(ChatInputMessageAccessoryPanel( + context: context, + contents: .reply(ChatInputMessageAccessoryPanel.Contents.Reply( + id: replyMessageSubject.messageId, + quote: replyMessageSubject.quote, + todoItemId: replyMessageSubject.todoItemId, + message: nil + )), + chatPeerId: chatPeerId, + action: { sourceView in + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + previousTapTimestamp = CFAbsoluteTimeGetCurrent() + interfaceInteraction?.presentReplyOptions(sourceView) + }, + dismiss: { _ in + interfaceInteraction?.setupReplyMessage(nil, nil, { _, f in f() }) + } + ))) + } else { + return nil + } + } else if let postSuggestionState = chatPresentationInterfaceState.interfaceState.postSuggestionState { + var previousTapTimestamp: Double? + return AnyComponentWithIdentity(id: "suggestPost", component: AnyComponent(ChatInputMessageAccessoryPanel( + context: context, + contents: .suggestPost(ChatInputMessageAccessoryPanel.Contents.SuggestPost( + state: postSuggestionState + )), + chatPeerId: chatPresentationInterfaceState.chatLocation.peerId, + action: { sourceView in + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + previousTapTimestamp = CFAbsoluteTimeGetCurrent() + interfaceInteraction?.presentSuggestPostOptions() + }, + dismiss: { _ in + interfaceInteraction?.dismissSuggestPost() + } + ))) + } + + return nil +} + +func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { + if "".isEmpty { + return nil + } + + if case .standard(.previewing) = chatPresentationInterfaceState.mode { + return nil + } + if let _ = chatPresentationInterfaceState.interfaceState.selectionState { + return nil + } + if chatPresentationInterfaceState.search != nil { + return nil + } + + switch chatPresentationInterfaceState.subject { + case .pinnedMessages, .messageOptions: + return nil + default: + break + } + + if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage, chatPresentationInterfaceState.interfaceState.postSuggestionState == nil { + let _ = editMessage + return nil } else if let urlPreview = chatPresentationInterfaceState.urlPreview, !chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) { if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { previewPanelNode.interfaceInteraction = interfaceInteraction diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 2e3a1c482f..8db0456530 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -8,8 +8,9 @@ import ChatInputPanelNode import ChatBotStartInputPanelNode import ChatChannelSubscriberInputPanelNode import ChatMessageSelectionInputPanelNode +import ChatControllerInteraction -func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { +func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { return (nil, nil) } @@ -36,6 +37,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -47,6 +49,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { if let currentPanel = (currentPanel as? ChatMessageSelectionInputPanelNode) ?? (currentSecondaryPanel as? ChatMessageSelectionInputPanelNode) { currentPanel.selectedMessages = selectionState.selectedIds + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateTheme(theme: chatPresentationInterfaceState.theme) selectionPanel = currentPanel @@ -54,12 +57,14 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState let panel = ChatMessageSelectionInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context panel.selectedMessages = selectionState.selectedIds + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction selectionPanel = panel } } if let currentPanel = (currentPanel as? ChatTagSearchInputPanelNode) ?? (currentSecondaryPanel as? ChatTagSearchInputPanelNode) { + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction return (currentPanel, selectionPanel) } else { @@ -70,6 +75,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState let panel = ChatTagSearchInputPanelNode(theme: chatPresentationInterfaceState.theme, alwaysShowTotalMessagesCount: alwaysShowTotalMessagesCount) panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, selectionPanel) } @@ -83,6 +89,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let _ = chatPresentationInterfaceState.reportReason { if let currentPanel = (currentPanel as? ChatMessageReportInputPanelNode) ?? (currentSecondaryPanel as? ChatMessageReportInputPanelNode) { currentPanel.selectedMessages = selectionState.selectedIds + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return (currentPanel, nil) @@ -90,12 +97,14 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState let panel = ChatMessageReportInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context panel.selectedMessages = selectionState.selectedIds + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } } else { if let currentPanel = (currentPanel as? ChatMessageSelectionInputPanelNode) ?? (currentSecondaryPanel as? ChatMessageSelectionInputPanelNode) { currentPanel.selectedMessages = selectionState.selectedIds + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateTheme(theme: chatPresentationInterfaceState.theme) return (currentPanel, nil) @@ -103,6 +112,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState let panel = ChatMessageSelectionInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context panel.selectedMessages = selectionState.selectedIds + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -114,6 +124,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -122,11 +133,13 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if chatPresentationInterfaceState.isPremiumRequiredForMessaging { if let currentPanel = (currentPanel as? ChatPremiumRequiredInputPanelNode) ?? (currentSecondaryPanel as? ChatPremiumRequiredInputPanelNode) { + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction return (currentPanel, nil) } else { let panel = ChatPremiumRequiredInputPanelNode(theme: chatPresentationInterfaceState.theme) panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -134,12 +147,14 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if chatPresentationInterfaceState.peerIsBlocked, let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo == nil { if let currentPanel = (currentPanel as? ChatUnblockInputPanelNode) ?? (currentSecondaryPanel as? ChatUnblockInputPanelNode) { + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return (currentPanel, nil) } else { let panel = ChatUnblockInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -153,6 +168,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -166,6 +182,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -176,6 +193,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -192,6 +210,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = SecretChatHandshakeStatusInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -201,6 +220,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = DeleteChatInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -216,6 +236,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = DeleteChatInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -241,6 +262,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -251,6 +273,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -278,6 +301,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -290,6 +314,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -300,6 +325,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -312,6 +338,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -326,6 +353,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -339,6 +367,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -350,6 +379,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) @@ -367,6 +397,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = DeleteChatInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -380,6 +411,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -412,6 +444,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -422,6 +455,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme) panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -445,6 +479,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else { let panel = ChatRestrictedInputPanelNode() panel.context = context + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction return (panel, nil) } @@ -457,6 +492,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if displayInputTextPanel { if let currentPanel = (currentPanel as? ChatTextInputPanelNode) ?? (currentSecondaryPanel as? ChatTextInputPanelNode) { + currentPanel.chatControllerInteraction = chatControllerInteraction currentPanel.interfaceInteraction = interfaceInteraction return (currentPanel, nil) } else { @@ -469,6 +505,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState interfaceInteraction?.presentController(controller, nil) }) + panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context return (panel, nil) diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index 79d7e1d41b..306c0d879e 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -15,6 +15,8 @@ import ChatSendButtonRadialStatusNode import ChatSendMessageActionUI import ComponentFlow import AnimatedCountLabelNode +import GlassBackgroundComponent +import ComponentDisplayAdapters private final class EffectBadgeView: UIView { private let context: AccountContext @@ -131,10 +133,12 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction private let presentationContext: ChatPresentationContext? private let strings: PresentationStrings + let micButtonBackgroundView: GlassBackgroundView + let micButtonTintMaskView: UIImageView let micButton: ChatTextInputMediaRecordingButton + let sendContainerNode: ASDisplayNode - let backdropNode: ChatMessageBubbleBackdrop - let backgroundNode: ASDisplayNode + let sendButtonBackgroundView: GlassBackgroundView let sendButton: HighlightTrackingButtonNode var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? var sendButtonHasApplyIcon = false @@ -142,7 +146,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction let textNode: ImmediateAnimatedCountLabelNode - let expandMediaInputButton: HighlightableButtonNode + let expandMediaInputButton: HighlightTrackingButton + private let expandMediaInputButtonBackgroundView: GlassBackgroundView + private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView private var effectBadgeView: EffectBadgeView? var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)? @@ -165,22 +171,31 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction let theme = presentationInterfaceState.theme let strings = presentationInterfaceState.strings self.strings = strings - + + self.micButtonBackgroundView = GlassBackgroundView() + self.micButtonTintMaskView = UIImageView() + self.micButtonTintMaskView.tintColor = .black self.micButton = ChatTextInputMediaRecordingButton(context: context, theme: theme, pause: true, strings: strings, presentController: presentController) + self.micButton.animationOutput = self.micButtonTintMaskView + self.micButtonBackgroundView.maskContentView.addSubview(self.micButtonTintMaskView) self.sendContainerNode = ASDisplayNode() self.sendContainerNode.layer.allowsGroupOpacity = true - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor - self.backgroundNode.clipsToBounds = true - self.backdropNode = ChatMessageBubbleBackdrop() + self.sendButtonBackgroundView = GlassBackgroundView() self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil) self.textNode = ImmediateAnimatedCountLabelNode() self.textNode.isUserInteractionEnabled = false - self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .circle(36.0)) + self.expandMediaInputButton = HighlightTrackingButton() + self.expandMediaInputButtonBackgroundView = GlassBackgroundView() + self.expandMediaInputButtonBackgroundView.isUserInteractionEnabled = false + self.expandMediaInputButton.addSubview(self.expandMediaInputButtonBackgroundView) + self.expandMediaInputButtonIcon = GlassBackgroundView.ContentImageView() + self.expandMediaInputButtonBackgroundView.contentView.addSubview(self.expandMediaInputButtonIcon) + self.expandMediaInputButtonIcon.image = PresentationResourcesChat.chatInputPanelExpandButtonImage(presentationInterfaceState.theme) + self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor super.init() @@ -208,18 +223,25 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } self.micButton.layer.allowsGroupOpacity = true + self.view.addSubview(self.micButtonBackgroundView) self.view.addSubview(self.micButton) self.addSubnode(self.sendContainerNode) - self.sendContainerNode.addSubnode(self.backgroundNode) - if let presentationContext = presentationContext { - let graphics = PresentationResourcesChat.principalGraphics(theme: theme, wallpaper: presentationInterfaceState.chatWallpaper, bubbleCorners: presentationInterfaceState.bubbleCorners) - self.backdropNode.setType(type: .outgoing(.None), theme: ChatPresentationThemeData(theme: theme, wallpaper: presentationInterfaceState.chatWallpaper), essentialGraphics: graphics, maskMode: true, backgroundNode: presentationContext.backgroundNode) - self.backgroundNode.addSubnode(self.backdropNode) - } + self.sendContainerNode.view.addSubview(self.sendButtonBackgroundView) self.sendContainerNode.addSubnode(self.sendButton) self.sendContainerNode.addSubnode(self.textNode) - self.addSubnode(self.expandMediaInputButton) + self.view.addSubview(self.expandMediaInputButton) + + self.expandMediaInputButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.expandMediaInputButton.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false) + } else if let presentationLayer = self.expandMediaInputButton.layer.presentation() { + self.expandMediaInputButton.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) + } + } } override func didLoad() { @@ -236,30 +258,18 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } self.micButtonPointerInteraction = PointerInteraction(view: self.micButton, style: .circle(36.0)) - self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.backgroundNode.view, style: .lift) + self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.sendButtonBackgroundView, style: .lift) } func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { self.micButton.updateTheme(theme: theme) - self.expandMediaInputButton.setImage(PresentationResourcesChat.chatInputPanelExpandButtonImage(theme), for: []) - - self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor - - if [.day, .night].contains(theme.referenceTheme.baseTheme) && !theme.chat.message.outgoing.bubble.withWallpaper.hasSingleFillColor { - self.backdropNode.isHidden = false - } else { - self.backdropNode.isHidden = true - } - - let graphics = PresentationResourcesChat.principalGraphics(theme: theme, wallpaper: wallpaper, bubbleCorners: .init(mainRadius: 1, auxiliaryRadius: 1, mergeBubbleCorners: false)) - self.backdropNode.setType(type: .outgoing(.None), theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), essentialGraphics: graphics, maskMode: false, backgroundNode: self.presentationContext?.backgroundNode) + self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor } private var absoluteRect: (CGRect, CGSize)? func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { let previousContaierSize = self.absoluteRect?.1 self.absoluteRect = (rect, containerSize) - self.backdropNode.update(rect: rect, within: containerSize, transition: transition) if let previousContaierSize, previousContaierSize != containerSize { Queue.mainQueue().after(0.2) { @@ -322,26 +332,30 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.textNode.isHidden = true } + transition.updateFrame(view: self.micButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.micButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: ComponentTransition(transition)) + transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) self.micButton.layoutItems() + transition.updateFrame(view: self.sendButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: innerSize)) + self.sendButtonBackgroundView.update(size: innerSize, cornerRadius: innerSize.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.actionControlFillColor, transition: ComponentTransition(transition)) transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: innerSize)) transition.updateFrame(node: self.sendContainerNode, frame: CGRect(origin: CGPoint(), size: innerSize)) - let backgroundSize = CGSize(width: innerSize.width - 11.0, height: 33.0) + let backgroundSize = CGSize(width: innerSize.width, height: 40.0) let backgroundFrame = CGRect(origin: CGPoint(x: showTitle ? 5.0 + UIScreenPixel : floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - self.backgroundNode.cornerRadius = backgroundSize.height / 2.0 - transition.updateFrame(node: self.backdropNode, frame: CGRect(origin: CGPoint(x: -2.0, y: -2.0), size: CGSize(width: innerSize.width + 12.0, height: size.height + 2.0))) - if let (rect, containerSize) = self.absoluteRect { - self.backdropNode.update(rect: rect, within: containerSize) + transition.updateFrame(view: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(view: self.expandMediaInputButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: ComponentTransition(transition)) + if let image = self.expandMediaInputButtonIcon.image { + let expandIconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size) + transition.updatePosition(layer: self.expandMediaInputButtonIcon.layer, position: expandIconFrame.center) + transition.updateBounds(layer: self.expandMediaInputButtonIcon.layer, bounds: CGRect(origin: CGPoint(), size: expandIconFrame.size)) + transition.updateTransformScale(layer: self.expandMediaInputButtonIcon.layer, scale: CGPoint(x: 1.0, y: isMediaInputExpanded ? 1.0 : -1.0)) } - transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) - let expanded = isMediaInputExpanded - transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0)) - if let currentMessageEffectId { let effectBadgeView: EffectBadgeView if let current = self.effectBadgeView { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index fa0851362f..5729398bea 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -47,55 +47,97 @@ import TelegramStringFormatting import TextNodeWithEntities import DeviceModel import PhotoResources +import GlassBackgroundComponent +import ComponentDisplayAdapters +import ChatInputAccessoryPanel private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) -private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { +private final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundView.ContentView { private var item: ChatTextInputAccessoryItem private var theme: PresentationTheme private var strings: PresentationStrings private var width: CGFloat - private let iconImageNode: ASImageNode + private let iconImageView: UIImageView + private let tintMaskIconImageView: UIImageView + private var textView: ImmediateTextView? + private var tintMaskTextView: ImmediateTextView? private var animationView: ComponentView? - private var imageEdgeInsets = UIEdgeInsets() + private var tintMaskAnimationView: UIImageView? + + override static var layerClass: AnyClass { + return GlassBackgroundView.ContentLayer.self + } + + let tintMask = UIView() init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) { self.item = item self.theme = theme self.strings = strings - self.iconImageNode = ASImageNode() + self.iconImageView = UIImageView() + self.tintMaskIconImageView = UIImageView() - let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings) + let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings) - self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings) + self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings) - super.init(pointerStyle: .circle(30.0)) + super.init(frame: CGRect()) + + (self.layer as? GlassBackgroundView.ContentLayer)?.targetLayer = self.tintMask.layer self.isAccessibilityElement = true self.accessibilityTraits = [.button] - self.iconImageNode.isUserInteractionEnabled = false - self.addSubnode(self.iconImageNode) + self.iconImageView.isUserInteractionEnabled = false + self.addSubview(self.iconImageView) + + self.tintMask.addSubview(self.tintMaskIconImageView) switch item { case .input, .botInput, .silentPost: - self.iconImageNode.isHidden = true + self.iconImageView.isHidden = true + self.tintMaskIconImageView.isHidden = self.iconImageView.isHidden self.animationView = ComponentView() + self.tintMaskAnimationView = UIImageView() default: break } - if let text = text { - self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal) + if let text { + if self.textView == nil { + let textView = ImmediateTextView() + self.textView = textView + self.addSubview(textView) + } + if self.tintMaskTextView == nil { + let tintMaskTextView = ImmediateTextView() + self.tintMaskTextView = tintMaskTextView + self.tintMask.addSubview(tintMaskTextView) + } + + self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) + self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) } else { - self.setAttributedTitle(NSAttributedString(), for: .normal) + if let textView = self.textView { + self.textView = nil + textView.removeFromSuperview() + } + if let tintMaskTextView = self.tintMaskTextView { + self.tintMaskTextView = nil + tintMaskTextView.removeFromSuperview() + } } - self.iconImageNode.image = image - self.iconImageNode.alpha = alpha - self.imageEdgeInsets = insets + self.iconImageView.image = image + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor + self.iconImageView.alpha = alpha + + self.tintMaskIconImageView.image = self.iconImageView.image + self.tintMaskIconImageView.tintColor = .black + self.tintMaskIconImageView.alpha = self.iconImageView.alpha self.accessibilityLabel = accessibilityLabel @@ -114,23 +156,33 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + return result + } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme self.strings = strings - let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings) + let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings) - self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings) + self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings) - if let text = text { - self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal) - } else { - self.setAttributedTitle(NSAttributedString(), for: .normal) + if let text { + self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) + self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) } - self.iconImageNode.image = image - self.imageEdgeInsets = insets - self.iconImageNode.alpha = alpha + self.iconImageView.image = image + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor + self.iconImageView.alpha = alpha + + self.tintMaskIconImageView.image = self.iconImageView.image + self.tintMaskIconImageView.tintColor = .black + self.tintMaskIconImageView.alpha = self.iconImageView.alpha self.accessibilityLabel = accessibilityLabel } @@ -143,12 +195,12 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { switch item { case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode): switch inputMode { - case .keyboard: - return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets()) - case .stickers, .emoji: - return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets()) - case .bot: - return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets()) + case .keyboard: + return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets()) + case .stickers, .emoji: + return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets()) + case .bot: + return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets()) } case .commands: return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets()) @@ -191,22 +243,22 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { let previousItem = self.item self.item = item - let (updatedImage, text, _, _, _) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: self.theme, strings: self.strings) + let (updatedImage, text, _, _, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: self.theme, strings: self.strings) - if let image = self.iconImageNode.image { - self.iconImageNode.image = updatedImage + if let image = self.iconImageView.image { + self.iconImageView.image = updatedImage + self.tintMaskIconImageView.image = updatedImage let bottomInset: CGFloat = 0.0 let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size) - self.iconImageNode.frame = imageFrame + self.iconImageView.frame = imageFrame + self.tintMaskIconImageView.frame = imageFrame if let animationView = self.animationView { - let width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: "", strings: self.strings) - //let iconSize = CGSize(width: width, height: width) + let width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: "", strings: self.strings) let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width)) - //let colorKeys: [String] = ["__allcolors__"] let animationName: String var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end) @@ -304,16 +356,11 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } - /*var colors: [String: UIColor] = [:] - for colorKey in colorKeys { - colors[colorKey] = self.theme.chat.inputPanel.inputControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0) - }*/ - let animationSize = animationView.update( transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: animationName), - color: self.theme.chat.inputPanel.inputControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0) + color: self.theme.chat.inputPanel.inputControlColor )), environment: {}, containerSize: animationFrame.size @@ -321,9 +368,17 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { if let view = animationView.view as? LottieComponent.View { view.isUserInteractionEnabled = false if view.superview == nil { - self.view.addSubview(view) + view.output = self.tintMaskAnimationView + self.addSubview(view) + if let tintMaskAnimationView = self.tintMaskAnimationView { + self.tintMask.addSubview(tintMaskAnimationView) + } + } + let animationFrameValue = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize) + view.frame = animationFrameValue + if let tintMaskAnimationView = self.tintMaskAnimationView { + tintMaskAnimationView.frame = animationFrameValue } - view.frame = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize) if case .animating = animationMode { view.playOnce() @@ -332,10 +387,18 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } - if let text = text { - self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: self.theme.chat.inputPanel.inputControlColor), for: .normal) - } else { - self.setAttributedTitle(NSAttributedString(), for: .normal) + if let text { + self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) + self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) + } + + if let textView = self.textView, let tintMaskTextView = self.tintMaskTextView { + let textSize = textView.updateLayout(CGSize(width: 100.0, height: 100.0)) + let _ = tintMaskTextView.updateLayout(CGSize(width: 100.0, height: 100.0)) + + let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: floor((size.height - textSize.height) * 0.5)), size: textSize) + textView.frame = textFrame + tintMaskTextView.frame = textFrame } } @@ -396,41 +459,6 @@ private func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresen return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right) } -private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, CGFloat, UIImage)? -private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackgroundColor: UIColor?, strokeColor: UIColor, diameter: CGFloat, strokeWidth: CGFloat) -> UIImage? { - if let backgroundColor = backgroundColor, let current = currentTextInputBackgroundImage { - if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) && current.3.isEqual(to: strokeWidth) { - return current.4 - } - } - - let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in - context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - - if let inputBackgroundColor = inputBackgroundColor { - context.setBlendMode(.normal) - context.setFillColor(inputBackgroundColor.cgColor) - } else { - context.setBlendMode(.clear) - context.setFillColor(UIColor.clear.cgColor) - } - context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - - context.setBlendMode(.normal) - context.setStrokeColor(strokeColor.cgColor) - context.setLineWidth(strokeWidth) - context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) - })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) - if let image = image { - if let backgroundColor = backgroundColor { - currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, strokeWidth, image) - } - return image - } else { - return nil - } -} - enum ChatTextInputPanelPasteData { case images([UIImage]) case video(Data) @@ -531,19 +559,22 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { let clippingNode: ASDisplayNode - var textPlaceholderNode: ImmediateTextNodeWithEntities + let textPlaceholderNode: ImmediateTextNodeWithEntities + let tintMaskTextPlaceholderNode: ImmediateTextNodeWithEntities + var textLockIconNode: ASImageNode? var contextPlaceholderNode: TextNode? var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? - let textInputContainerBackgroundNode: ASImageNode + let textInputContainerBackgroundView: GlassBackgroundView let textInputContainer: ASDisplayNode + let textInputNodeClippingContainer: ASDisplayNode + let textInputSeparator: GlassBackgroundView.ContentColorView var textInputNode: ChatInputTextNode? var dustNode: InvisibleInkDustNode? var customEmojiContainerView: CustomEmojiContainerView? let textInputBackgroundNode: ASImageNode var textInputBackgroundTapRecognizer: TouchDownGestureRecognizer? - private var transparentTextInputBackgroundImage: UIImage? let actionButtons: ChatTextInputActionButtonsNode private let slowModeButton: BoostSlowModeButton var mediaRecordingAccessibilityArea: AccessibilityAreaNode? @@ -562,7 +593,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let sendAsAvatarContainerNode: ContextControllerSourceNode private let sendAsAvatarNode: AvatarNode - let attachmentButton: HighlightableButtonNode + let attachmentButton: HighlightTrackingButton + let attachmentButtonBackground: GlassBackgroundView + let attachmentButtonIcon: GlassBackgroundView.ContentImageView let attachmentButtonDisabledNode: HighlightableButtonNode var attachmentImageNode: TransformImageNode? @@ -580,7 +613,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var viewOnce = false let viewOnceButton: ChatRecordingViewOnceButtonNode - private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] + private var accessoryPanel: (component: AnyComponentWithIdentity, view: ComponentView)? + + private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)? private var leftMenuInset: CGFloat = 0.0 @@ -681,9 +716,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } if updateAccessoryButtons { - var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] + var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] for item in accessoryItems { - var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)? + var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)? for i in 0 ..< self.accessoryItemButtons.count { if self.accessoryItemButtons[i].0.key == item.key { itemAndButton = self.accessoryItemButtons[i] @@ -693,14 +728,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } if itemAndButton == nil { - let button = AccessoryItemIconButtonNode(item: item, theme: currentState.theme, strings: currentState.strings) - button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside) + let button = AccessoryItemIconButton(item: item, theme: currentState.theme, strings: currentState.strings) + button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: .touchUpInside) itemAndButton = (item, button) } updatedButtons.append(itemAndButton!) } for (_, button) in self.accessoryItemButtons { - button.removeFromSupernode() + button.removeFromSuperview() } self.accessoryItemButtons = updatedButtons } @@ -767,7 +802,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private let textInputViewInternalInsets: UIEdgeInsets private let accessoryButtonSpacing: CGFloat = 0.0 - private let accessoryButtonInset: CGFloat = 2.0 + private let accessoryButtonInset: CGFloat = 6.0 private var spoilersRevealed = false @@ -786,7 +821,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.presentationInterfaceState = presentationInterfaceState self.presentationContext = presentationContext - self.textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) + self.textInputViewInternalInsets = UIEdgeInsets(top: 5.0, left: 16.0, bottom: 4.0, right: 15.0) var hasSpoilers = true var hasQuotes = true @@ -797,16 +832,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.inputMenu = TextInputMenu(hasSpoilers: hasSpoilers, hasQuotes: hasQuotes) self.clippingNode = ASDisplayNode() - self.clippingNode.clipsToBounds = true + self.clippingNode.clipsToBounds = false - self.textInputContainerBackgroundNode = ASImageNode() - self.textInputContainerBackgroundNode.isUserInteractionEnabled = false - self.textInputContainerBackgroundNode.displaysAsynchronously = false + self.textInputContainerBackgroundView = GlassBackgroundView(frame: CGRect()) + self.textInputContainerBackgroundView.isUserInteractionEnabled = false self.textInputContainer = ASDisplayNode() - self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode) + self.textInputContainer.view.addSubview(self.textInputContainerBackgroundView) self.textInputContainer.clipsToBounds = true + self.textInputNodeClippingContainer = ASDisplayNode() + self.textInputNodeClippingContainer.clipsToBounds = true + + self.textInputSeparator = GlassBackgroundView.ContentColorView() + self.textInputContainerBackgroundView.contentView.addSubview(self.textInputSeparator) + self.textInputBackgroundNode = ASImageNode() self.textInputBackgroundNode.displaysAsynchronously = false self.textInputBackgroundNode.displayWithoutProcessing = true @@ -824,6 +864,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.textPlaceholderNode.maximumNumberOfLines = 1 self.textPlaceholderNode.isUserInteractionEnabled = false + //TODO:release add tinted output instead + self.tintMaskTextPlaceholderNode = ImmediateTextNodeWithEntities() + self.tintMaskTextPlaceholderNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ) + self.tintMaskTextPlaceholderNode.contentMode = .topLeft + self.tintMaskTextPlaceholderNode.contentsScale = UIScreenScale + self.tintMaskTextPlaceholderNode.maximumNumberOfLines = 1 + self.menuButton = HighlightTrackingButtonNode() self.menuButton.clipsToBounds = true self.menuButton.cornerRadius = 16.0 @@ -849,10 +902,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.sendAsAvatarContainerNode.animateScale = false self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) - self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle(36.0)) + self.attachmentButton = HighlightTrackingButton() self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia self.attachmentButton.accessibilityTraits = [.button] self.attachmentButton.isAccessibilityElement = true + + self.attachmentButtonBackground = GlassBackgroundView(frame: CGRect()) + self.attachmentButtonBackground.isUserInteractionEnabled = false + self.attachmentButton.addSubview(self.attachmentButtonBackground) + + self.attachmentButtonIcon = GlassBackgroundView.ContentImageView() + self.attachmentButtonIcon.isUserInteractionEnabled = false + self.attachmentButtonBackground.contentView.addSubview(self.attachmentButtonIcon) + self.attachmentButtonDisabledNode = HighlightableButtonNode() self.searchLayoutClearButton = HighlightableButton() self.searchLayoutClearImageNode = ASImageNode() @@ -951,7 +1013,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) + self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) + self.attachmentButton.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.attachmentButtonIcon.layer.removeAnimation(forKey: "opacity") + self.attachmentButtonIcon.alpha = 0.4 + self.attachmentButtonIcon.layer.allowsGroupOpacity = true + } else { + self.attachmentButtonIcon.alpha = 1.0 + self.attachmentButtonIcon.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + self.attachmentButtonIcon.layer.allowsGroupOpacity = false + } + } + } self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in @@ -1031,7 +1106,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.actionButtons.sendContainerNode.alpha = 0.0 self.actionButtons.updateAccessibility() - self.actionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), forControlEvents: .touchUpInside) + self.actionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), for: .touchUpInside) self.actionButtons.expandMediaInputButton.alpha = 0.0 self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside) @@ -1040,7 +1115,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.clippingNode.addSubnode(self.textInputContainer) self.clippingNode.addSubnode(self.textInputBackgroundNode) - self.clippingNode.addSubnode(self.textPlaceholderNode) + self.textInputContainerBackgroundView.contentView.addSubview(self.textPlaceholderNode.view) + self.textInputContainerBackgroundView.maskContentView.addSubview(self.tintMaskTextPlaceholderNode.view) self.menuButton.addSubnode(self.menuButtonBackgroundNode) self.menuButton.addSubnode(self.menuButtonClippingNode) @@ -1053,7 +1129,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.clippingNode.addSubnode(self.sendAsAvatarButtonNode) self.clippingNode.addSubnode(self.menuButton) - self.clippingNode.addSubnode(self.attachmentButton) + self.clippingNode.view.addSubview(self.attachmentButton) self.clippingNode.addSubnode(self.attachmentButtonDisabledNode) self.clippingNode.addSubnode(self.startButton) @@ -1162,14 +1238,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch paragraphStyle.minimumLineHeight = 20.0 textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle] - textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false textInputNode.delegate = self textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) textInputNode.keyboardAppearance = keyboardAppearance textInputNode.tintColor = tintColor textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0) - self.textInputContainer.addSubnode(textInputNode) + self.textInputContainer.addSubnode(self.textInputNodeClippingContainer) + self.textInputNodeClippingContainer.addSubnode(textInputNode) textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled self.textInputNode = textInputNode @@ -1184,9 +1260,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch for (_, button) in self.accessoryItemButtons { if firstButton { firstButton = false - accessoryButtonsWidth += accessoryButtonInset + accessoryButtonsWidth += self.accessoryButtonInset } else { - accessoryButtonsWidth += accessoryButtonSpacing + accessoryButtonsWidth += self.accessoryButtonSpacing } accessoryButtonsWidth += button.buttonWidth } @@ -1206,7 +1282,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled - //self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) textInputNode.textView.onUpdateLayout = { [weak self] in guard let self else { @@ -1293,12 +1368,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return max(33.0, maxHeight - (textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom)) } - private func calculateTextFieldMetrics(width: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat) { - let accessoryButtonInset = self.accessoryButtonInset - let accessoryButtonSpacing = self.accessoryButtonSpacing - + private func calculateTextFieldMetrics(width: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat, isOverflow: Bool) { var textFieldInsets = self.textFieldInsets(metrics: metrics) - if self.actionButtons.frame.width > 44.0 { + if self.actionButtons.frame.width > 40.0 { textFieldInsets.right = self.actionButtons.frame.width - 2.0 } @@ -1309,9 +1381,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch for (_, button) in self.accessoryItemButtons { if firstButton { firstButton = false - accessoryButtonsWidth += accessoryButtonInset + accessoryButtonsWidth += self.accessoryButtonInset } else { - accessoryButtonsWidth += accessoryButtonSpacing + accessoryButtonsWidth += self.accessoryButtonSpacing } accessoryButtonsWidth += button.buttonWidth } @@ -1326,7 +1398,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } - let textFieldHeight: CGFloat + var textFieldHeight: CGFloat + var isOverflow = false if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textInputViewRealInsets.right) @@ -1337,16 +1410,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let updatedMaxHeight = (CGFloat(maxNumberOfLines) * (22.0 + 2.0) + 10.0) - textFieldHeight = max(textFieldMinHeight, min(updatedMaxHeight, unboundTextFieldHeight)) + textFieldHeight = max(textFieldMinHeight, unboundTextFieldHeight) + isOverflow = textFieldHeight > updatedMaxHeight + textFieldHeight = min(textFieldHeight, updatedMaxHeight) } else { textFieldHeight = textFieldMinHeight } - return (accessoryButtonsWidth, textFieldHeight) + return (accessoryButtonsWidth, textFieldHeight, isOverflow) } private func textFieldInsets(metrics: LayoutMetrics) -> UIEdgeInsets { - var insets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0) + var insets = UIEdgeInsets(top: 0.0, left: 54.0, bottom: 0.0, right: 54.0) if case .regular = metrics.widthClass, case .regular = metrics.heightClass { insets.top += 1.0 insets.bottom += 1.0 @@ -1495,7 +1570,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout( + width: CGFloat, + leftInset: CGFloat, + rightInset: CGFloat, + bottomInset: CGFloat, + additionalSideInsets: UIEdgeInsets, + maxHeight: CGFloat, + isSecondary: Bool, + transition: ContainedViewLayoutTransition, + interfaceState: ChatPresentationInterfaceState, + metrics: LayoutMetrics, + isMediaInputExpanded: Bool + ) -> CGFloat { let previousAdditionalSideInsets = self.validLayout?.4 self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) @@ -1777,7 +1864,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch }) self.menuButtonTextNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - menuTextSize = self.menuButtonTextNode.updateLayout(CGSize(width: width / 2.0 - 60.0, height: 44.0)) + menuTextSize = self.menuButtonTextNode.updateLayout(CGSize(width: width / 2.0 - 60.0, height: 40.0)) var updateSendButtonIcon = false if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) { @@ -1828,30 +1915,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.menuButtonBackgroundNode.backgroundColor = interfaceState.theme.chat.inputPanel.actionControlFillColor if isEditingMedia { - self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: []) + self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor } else { - self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) + self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor } self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) - let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) - let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight - - let strokeWidth: CGFloat - let backgroundColor: UIColor - if case let .color(color) = interfaceState.chatWallpaper, UIColor(rgb: color).isEqual(interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { - backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper - strokeWidth = 1.0 - UIScreenPixel - } else { - backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor - strokeWidth = UIScreenPixel - } - - self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, strokeWidth: strokeWidth) - self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, strokeWidth: strokeWidth) - self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor) - self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme) self.audioRecordingTimeNode?.updateTheme(theme: interfaceState.theme) @@ -1872,9 +1944,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if wasEditingMedia != isEditingMedia { if isEditingMedia { - self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: []) + self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor } else { - self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) + self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor } } } @@ -2015,8 +2089,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let presentationInterfaceState = self.presentationInterfaceState { textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) } - let minimalHeight: CGFloat = 14.0 + textFieldMinHeight - let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight + let minimalHeight: CGFloat = self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom + textFieldMinHeight + let minimalInputHeight: CGFloat = self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom + textFieldMinHeight var animatedTransition = true if case .immediate = transition { @@ -2035,11 +2109,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch updateAccessoryButtons = true } - var removeAccessoryButtons: [AccessoryItemIconButtonNode]? + var removeAccessoryButtons: [AccessoryItemIconButton]? if updateAccessoryButtons { - var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] + var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] for item in interfaceState.inputTextPanelState.accessoryItems { - var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)? + var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)? for i in 0 ..< self.accessoryItemButtons.count { if self.accessoryItemButtons[i].0.key == item.key { itemAndButton = self.accessoryItemButtons[i] @@ -2049,8 +2123,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } if itemAndButton == nil { - let button = AccessoryItemIconButtonNode(item: item, theme: interfaceState.theme, strings: interfaceState.strings) - button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside) + let button = AccessoryItemIconButton(item: item, theme: interfaceState.theme, strings: interfaceState.strings) + button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: .touchUpInside) itemAndButton = (item, button) } updatedButtons.append(itemAndButton!) @@ -2062,7 +2136,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } removeAccessoryButtons!.append(button) } else { - button.removeFromSupernode() + button.removeFromSuperview() } } self.accessoryItemButtons = updatedButtons @@ -2087,7 +2161,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var rightSlowModeInset: CGFloat = 0.0 var slowModeButtonSize: CGSize = .zero if let presentationInterfaceState = self.presentationInterfaceState, (presentationInterfaceState.boostsToUnrestrict ?? 0) > 0 { - slowModeButtonSize = self.slowModeButton.update(size: CGSize(width: width, height: 44.0), interfaceState: presentationInterfaceState) + slowModeButtonSize = self.slowModeButton.update(size: CGSize(width: width, height: 40.0), interfaceState: presentationInterfaceState) rightSlowModeInset = max(0.0, slowModeButtonSize.width - 33.0) } self.rightSlowModeInset = rightSlowModeInset @@ -2099,7 +2173,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var leftInset = leftInset var textInputBackgroundWidthOffset: CGFloat = 0.0 - var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 2.0 - UIScreenPixel + var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 8.0 if !displayMediaButton { attachmentButtonX = -40.0 let inputFieldAdditionalWidth = 40.0 - 4.0 @@ -2108,7 +2182,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } let baseWidth = width - leftInset - leftMenuInset - rightInset - rightSlowModeInset - let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) + let (accessoryButtonsWidth, textFieldHeight, isTextFieldOverflow) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if displayBotStartButton { panelHeight += 27.0 @@ -2479,11 +2553,436 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch leftInset += leftMenuInset - let attachmentButtonFrame = CGRect(origin: CGPoint(x: attachmentButtonX, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)) + var composeButtonsOffset: CGFloat = 0.0 + if self.extendedSearchLayout { + composeButtonsOffset = 40.0 + textInputBackgroundWidthOffset = 36.0 + } + + self.updateCounterTextNode(transition: transition) + + if inputHasText || self.extendedSearchLayout { + hideMicButton = true + } + + self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated) + + var textInputViewRealInsets = UIEdgeInsets() + if let presentationInterfaceState = self.presentationInterfaceState { + textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) + } + + var actionButtonsSize = CGSize(width: 40.0, height: 40.0) + if let presentationInterfaceState = self.presentationInterfaceState { + var showTitle = false + if !self.actionButtons.sendContainerNode.alpha.isZero { + if let _ = presentationInterfaceState.sendPaidMessageStars { + showTitle = true + } else if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + default: + break + } + } + } + actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) + } + + var textFieldInsets = self.textFieldInsets(metrics: metrics) + if actionButtonsSize.width > 40.0 { + textFieldInsets.right = actionButtonsSize.width - 2.0 + } + if additionalSideInsets.right > 0.0 { + textFieldInsets.right += additionalSideInsets.right / 3.0 + } + + var contentHeight: CGFloat = 0.0 + + var accessoryPanel: AnyComponentWithIdentity? + if let context = self.context { + accessoryPanel = textInputAccessoryPanel( + context: context, + chatPresentationInterfaceState: interfaceState, + chatControllerInteraction: self.chatControllerInteraction, + interfaceInteraction: self.interfaceInteraction + ) + } + + let alphaTransitionIn: ContainedViewLayoutTransition = transition.isAnimated ? ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) : .immediate + let alphaTransitionOut: ContainedViewLayoutTransition = transition.isAnimated ? ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) : .immediate + + let accessoryPanelAnimationBlurRadius: CGFloat = 10.0 + var removedAccessoryPanelView: UIView? + + if let currentAccessoryPanel = self.accessoryPanel, currentAccessoryPanel.component.id != accessoryPanel?.id { + self.accessoryPanel = nil + if let accessoryPanelView = currentAccessoryPanel.view.view { + removedAccessoryPanelView = accessoryPanelView + } + } + + let textInputWidth = baseWidth - textFieldInsets.left - textFieldInsets.right + let textInputHeight = panelHeight - textFieldInsets.top - textFieldInsets.bottom + + if let accessoryPanel { + var accessoryPanelTransition = ComponentTransition(transition) + let accessoryPanelView: ComponentView + if let current = self.accessoryPanel { + accessoryPanelView = current.view + } else { + accessoryPanelTransition = .immediate + accessoryPanelView = ComponentView() + } + self.accessoryPanel = (accessoryPanel, accessoryPanelView) + + let accessoryPanelSize = accessoryPanelView.update( + transition: accessoryPanelTransition, + component: accessoryPanel.component, + environment: { + ChatInputAccessoryPanelEnvironment( + theme: interfaceState.theme, + strings: interfaceState.strings, + nameDisplayOrder: interfaceState.nameDisplayOrder, + dateTimeFormat: interfaceState.dateTimeFormat + ) + }, + containerSize: CGSize(width: textInputWidth, height: 10000.0) + ) + + let accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: accessoryPanelSize) + if let accessoryPanelComponentView = accessoryPanelView.view { + if accessoryPanelComponentView.superview == nil { + self.textInputContainer.view.addSubview(accessoryPanelComponentView) + accessoryPanelComponentView.frame = accessoryPanelFrame.offsetBy(dx: 0.0, dy: self.textInputNodeClippingContainer.frame.minY - accessoryPanelFrame.height) + accessoryPanelComponentView.alpha = 0.0 + + if transition.isAnimated { + ComponentTransition(alphaTransitionIn).animateBlur(layer: accessoryPanelComponentView.layer, fromRadius: accessoryPanelAnimationBlurRadius, toRadius: 0.0) + } + + if let accessoryPanelComponentView = accessoryPanelComponentView as? ChatInputAccessoryPanelView { + self.textInputContainerBackgroundView.maskContentView.addSubview(accessoryPanelComponentView.contentTintView) + accessoryPanelComponentView.contentTintView.frame = accessoryPanelFrame.offsetBy(dx: 0.0, dy: self.textInputNodeClippingContainer.frame.minY - accessoryPanelFrame.height) + accessoryPanelComponentView.contentTintView.alpha = 0.0 + + if transition.isAnimated { + ComponentTransition(alphaTransitionIn).animateBlur(layer: accessoryPanelComponentView.contentTintView.layer, fromRadius: accessoryPanelAnimationBlurRadius, toRadius: 0.0) + } + } + } + transition.updateFrame(view: accessoryPanelComponentView, frame: accessoryPanelFrame) + alphaTransitionIn.updateAlpha(layer: accessoryPanelComponentView.layer, alpha: 1.0) + + if let accessoryPanelComponentView = accessoryPanelComponentView as? ChatInputAccessoryPanelView { + transition.updateFrame(view: accessoryPanelComponentView.contentTintView, frame: accessoryPanelFrame) + alphaTransitionIn.updateAlpha(layer: accessoryPanelComponentView.contentTintView.layer, alpha: 1.0) + } + } + + contentHeight += accessoryPanelSize.height + } + + let textFieldTopContentOffset = contentHeight + + contentHeight += textInputHeight + contentHeight += textFieldInsets.bottom + + let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: textInputWidth, height: contentHeight) + + transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) + transition.updateFrame(view: self.textInputContainerBackgroundView, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) + + self.textInputContainerBackgroundView.update(size: textInputFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: ComponentTransition(transition)) + + transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) + + if let removedAccessoryPanelView { + if let removedAccessoryPanelView = removedAccessoryPanelView as? ChatInputAccessoryPanelView { + let contentTintView = removedAccessoryPanelView.contentTintView + ComponentTransition(alphaTransitionOut).animateBlur(layer: contentTintView.layer, fromRadius: 0.0, toRadius: accessoryPanelAnimationBlurRadius, removeOnCompletion: false) + transition.updateFrame(view: contentTintView, frame: CGRect(origin: CGPoint(x: contentTintView.frame.minX, y: textFieldTopContentOffset - contentTintView.bounds.height), size: contentTintView.bounds.size)) + alphaTransitionOut.updateAlpha(layer: contentTintView.layer, alpha: 0.0, completion: { [weak contentTintView] _ in + contentTintView?.removeFromSuperview() + }) + } + ComponentTransition(alphaTransitionOut).animateBlur(layer: removedAccessoryPanelView.layer, fromRadius: 0.0, toRadius: accessoryPanelAnimationBlurRadius, removeOnCompletion: false) + transition.updateFrame(view: removedAccessoryPanelView, frame: CGRect(origin: CGPoint(x: removedAccessoryPanelView.frame.minX, y: textFieldTopContentOffset - removedAccessoryPanelView.bounds.height), size: removedAccessoryPanelView.bounds.size)) + alphaTransitionOut.updateAlpha(layer: removedAccessoryPanelView.layer, alpha: 0.0, completion: { [weak removedAccessoryPanelView] _ in + removedAccessoryPanelView?.removeFromSuperview() + }) + } + + if let textInputNode = self.textInputNode { + textInputNode.textContainerInset = textInputViewRealInsets + let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top + textFieldTopContentOffset), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputHeight - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) + let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size + transition.updateFrame(node: self.textInputNodeClippingContainer, frame: CGRect(origin: CGPoint(x: textFieldFrame.minX - self.textInputViewInternalInsets.left, y: textFieldFrame.minY - self.textInputViewInternalInsets.top), size: CGSize(width: textFieldFrame.width + self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right, height: textFieldFrame.height + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom))) + + transition.updateFrame(view: self.textInputSeparator, frame: CGRect(origin: CGPoint(x: 15.0, y: textFieldTopContentOffset - UIScreenPixel), size: CGSize(width: textFieldFrame.width, height: UIScreenPixel))) + self.textInputSeparator.backgroundColor = interfaceState.theme.chat.inputPanel.inputPlaceholderColor + transition.updateAlpha(layer: self.textInputSeparator.layer, alpha: isTextFieldOverflow ? 1.0 : 0.0) + + textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: textFieldFrame.size) + textInputNode.updateLayout(size: textFieldFrame.size) + self.updateInputField(textInputFrame: textFieldFrame, transition: ComponentTransition(transition)) + if shouldUpdateLayout { + textInputNode.layout() + } + } + + if interfaceState.slowmodeState == nil || isScheduledMessages, let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder { + let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode) + let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let contextPlaceholderNode = placeholderApply() + if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode { + self.contextPlaceholderNode = nil + currentContextPlaceholderNode.removeFromSupernode() + } + + if self.contextPlaceholderNode !== contextPlaceholderNode { + contextPlaceholderNode.displaysAsynchronously = false + contextPlaceholderNode.isUserInteractionEnabled = false + self.contextPlaceholderNode = contextPlaceholderNode + self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) + + self.textInputContainerBackgroundView.contentView.insertSubview(contextPlaceholderNode.view, aboveSubview: self.textPlaceholderNode.view) + } + + let _ = placeholderApply() + + let placeholderTransition: ContainedViewLayoutTransition + if placeholderSize.size.width == contextPlaceholderNode.frame.width { + placeholderTransition = transition + } else { + placeholderTransition = .immediate + } + placeholderTransition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size)) + contextPlaceholderNode.alpha = audioRecordingItemsAlpha + } else if let contextPlaceholderNode = self.contextPlaceholderNode { + self.contextPlaceholderNode = nil + contextPlaceholderNode.removeFromSupernode() + self.textPlaceholderNode.alpha = 1.0 + self.tintMaskTextPlaceholderNode.alpha = 1.0 + } + + if let slowmodeState = interfaceState.slowmodeState, !isScheduledMessages && rightSlowModeInset.isZero { + let slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode + if let current = self.slowmodePlaceholderNode { + slowmodePlaceholderNode = current + } else { + slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme) + self.slowmodePlaceholderNode = slowmodePlaceholderNode + self.clippingNode.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode) + } + let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0)) + slowmodePlaceholderNode.updateState(slowmodeState) + slowmodePlaceholderNode.frame = placeholderFrame + slowmodePlaceholderNode.alpha = audioRecordingItemsAlpha + slowmodePlaceholderNode.updateLayout(size: placeholderFrame.size) + } else if let slowmodePlaceholderNode = self.slowmodePlaceholderNode { + self.slowmodePlaceholderNode = nil + slowmodePlaceholderNode.removeFromSupernode() + } + + if (interfaceState.slowmodeState != nil && rightSlowModeInset.isZero && !isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil { + self.textPlaceholderNode.isHidden = true + self.tintMaskTextPlaceholderNode.isHidden = true + self.slowmodePlaceholderNode?.isHidden = inputHasText + } else { + self.textPlaceholderNode.isHidden = inputHasText + self.tintMaskTextPlaceholderNode.isHidden = inputHasText + self.slowmodePlaceholderNode?.isHidden = true + } + + var nextButtonTopRight = CGPoint(x: textInputFrame.width - accessoryButtonInset - rightSlowModeInset, y: textInputFrame.height - minimalInputHeight) + for (item, button) in self.accessoryItemButtons.reversed() { + let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) + button.updateLayout(item: item, size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) + if button.superview == nil { + self.textInputContainerBackgroundView.contentView.addSubview(button) + button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0) + transition.updateFrame(layer: button.layer, frame: buttonFrame) + if animatedTransition { + button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + button.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + } + } else { + transition.updateFrame(layer: button.layer, frame: buttonFrame) + } + nextButtonTopRight.x -= buttonSize.width + nextButtonTopRight.x -= accessoryButtonSpacing + } + + let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top + textFieldTopContentOffset, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom) + self.currentTextInputBackgroundWidthOffset = textInputBackgroundWidthOffset + transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame) + transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha) + + let textPlaceholderSize: CGSize + let textPlaceholderMaxWidth: CGFloat = max(1.0, nextButtonTopRight.x - 12.0) + + if (updatedPlaceholder != nil && self.currentPlaceholder != updatedPlaceholder) || themeUpdated { + let currentPlaceholder = updatedPlaceholder ?? self.currentPlaceholder ?? "" + self.currentPlaceholder = currentPlaceholder + let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) + + let attributedPlaceholder = NSMutableAttributedString(string: currentPlaceholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + if placeholderHasStar, let range = attributedPlaceholder.string.range(of: "#") { + attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.foregroundColor, value: interfaceState.theme.chat.inputPanel.inputPlaceholderColor, range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) + } + + let attributedTintMaskPlaceholder = NSMutableAttributedString(string: currentPlaceholder, font: Font.regular(baseFontSize), textColor: .black) + if placeholderHasStar, let range = attributedPlaceholder.string.range(of: "#") { + attributedTintMaskPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) + attributedTintMaskPlaceholder.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(range, in: attributedPlaceholder.string)) + attributedTintMaskPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) + } + + self.textPlaceholderNode.attributedText = attributedPlaceholder + self.tintMaskTextPlaceholderNode.attributedText = attributedTintMaskPlaceholder + + self.textInputNode?.textView.accessibilityHint = currentPlaceholder + + let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) + let _ = self.tintMaskTextPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) + + if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { + self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer) + snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + + let _ = self.tintMaskTextPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) + if transition.isAnimated, let snapshotLayer = self.tintMaskTextPlaceholderNode.layer.snapshotContentTree() { + self.tintMaskTextPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.tintMaskTextPlaceholderNode.layer) + snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + self.tintMaskTextPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + + textPlaceholderSize = placeholderSize + } else { + textPlaceholderSize = self.textPlaceholderNode.bounds.size + } + + let textPlaceholderFrame: CGRect + if sendingTextDisabled { + textPlaceholderFrame = CGRect(origin: CGPoint(x: floor((textInputBackgroundFrame.width - textPlaceholderSize.width) / 2.0), y: self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: textPlaceholderSize) + + let textLockIconNode: ASImageNode + var textLockIconTransition = transition + if let current = self.textLockIconNode { + textLockIconNode = current + } else { + textLockIconTransition = .immediate + textLockIconNode = ASImageNode() + self.textLockIconNode = textLockIconNode + self.textPlaceholderNode.addSubnode(textLockIconNode) + + textLockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + } + + if let image = textLockIconNode.image { + textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size)) + } + } else { + textPlaceholderFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: textPlaceholderSize) + + if let textLockIconNode = self.textLockIconNode { + self.textLockIconNode = nil + textLockIconNode.removeFromSupernode() + } + } + transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame) + transition.updateFrame(node: self.tintMaskTextPlaceholderNode, frame: textPlaceholderFrame) + + let textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha + transition.updateAlpha(node: self.textPlaceholderNode, alpha: textPlaceholderAlpha) + transition.updateAlpha(node: self.tintMaskTextPlaceholderNode, alpha: textPlaceholderAlpha) + + if let removeAccessoryButtons = removeAccessoryButtons { + for button in removeAccessoryButtons { + let buttonFrame = CGRect(origin: CGPoint(x: button.frame.origin.x + additionalOffset, y: textInputFrame.maxY - minimalInputHeight), size: button.frame.size) + transition.updateFrame(layer: button.layer, frame: buttonFrame) + button.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak button] _ in + button?.removeFromSuperview() + }) + } + } + + let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 8.0 - actionButtonsSize.width + composeButtonsOffset, y: textInputFrame.maxY - actionButtonsSize.height), size: actionButtonsSize) + transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) + if let (rect, containerSize) = self.absoluteRect { + self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition) + } + + let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize) + transition.updateFrame(node: self.slowModeButton, frame: slowModeButtonFrame) + + if let _ = interfaceState.inputTextPanelState.mediaRecordingState { + let text: String = interfaceState.strings.VoiceOver_MessageContextSend + let mediaRecordingAccessibilityArea: AccessibilityAreaNode + var added = false + if let current = self.mediaRecordingAccessibilityArea { + mediaRecordingAccessibilityArea = current + } else { + added = true + mediaRecordingAccessibilityArea = AccessibilityAreaNode() + mediaRecordingAccessibilityArea.accessibilityLabel = text + mediaRecordingAccessibilityArea.accessibilityTraits = [.button, .startsMediaSession] + self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea + mediaRecordingAccessibilityArea.activate = { [weak self] in + if let self { + self.interfaceInteraction?.finishMediaRecording(.send(viewOnce: self.viewOnce)) + } + return true + } + self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) + } + self.actionButtons.isAccessibilityElement = false + let size: CGFloat = 120.0 + mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: actionButtonsFrame.midX - size / 2.0, y: actionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size)) + if added { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4, execute: { + [weak mediaRecordingAccessibilityArea] in + UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: mediaRecordingAccessibilityArea?.view) + }) + } + } else { + self.actionButtons.isAccessibilityElement = true + if let mediaRecordingAccessibilityArea = self.mediaRecordingAccessibilityArea { + self.mediaRecordingAccessibilityArea = nil + mediaRecordingAccessibilityArea.removeFromSupernode() + } + } + + let searchLayoutClearButtonSize = CGSize(width: 40.0, height: minimalHeight) + self.actionButtons.micButton.isHidden = additionalSideInsets.right > 0.0 + self.actionButtons.micButtonBackgroundView.isHidden = self.actionButtons.micButton.isHidden + + transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) + if let image = self.searchLayoutClearImageNode.image { + self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size) + } + + let attachmentButtonFrame = CGRect(origin: CGPoint(x: attachmentButtonX, y: textInputFrame.maxY - 40.0), size: CGSize(width: 40.0, height: 40.0)) + self.attachmentButtonBackground.frame = CGRect(origin: CGPoint(), size: attachmentButtonFrame.size) + self.attachmentButtonBackground.update(size: attachmentButtonFrame.size, cornerRadius: attachmentButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: isEditingMedia ? interfaceState.theme.chat.inputPanel.actionControlFillColor : interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: ComponentTransition(transition)) transition.updateFrame(layer: self.attachmentButton.layer, frame: attachmentButtonFrame) transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame) + if let image = self.attachmentButtonIcon.image { + transition.updateFrame(view: self.attachmentButtonIcon, frame: CGRect(origin: CGPoint(x: floor((attachmentButtonFrame.width - image.size.width) * 0.5), y: floor((attachmentButtonFrame.height - image.size.height) * 0.5)), size: image.size)) + } + if let context = self.context, let interfaceState = self.presentationInterfaceState, let editMessageState = interfaceState.editMessageState, let updatedMediaReference = editMessageState.mediaReference { let attachmentImageNode: TransformImageNode if let current = self.attachmentImageNode { @@ -2538,278 +3037,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.attachmentImageNode = nil attachmentImageNode.removeFromSupernode() } - - var composeButtonsOffset: CGFloat = 0.0 - if self.extendedSearchLayout { - composeButtonsOffset = 44.0 - textInputBackgroundWidthOffset = 36.0 - } - - self.updateCounterTextNode(transition: transition) - - if inputHasText || self.extendedSearchLayout { - hideMicButton = true - } - - self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated) - - var actionButtonsSize = CGSize(width: 44.0, height: minimalHeight) - if let presentationInterfaceState = self.presentationInterfaceState { - var showTitle = false - if !self.actionButtons.sendContainerNode.alpha.isZero { - if let _ = presentationInterfaceState.sendPaidMessageStars { - showTitle = true - } else if case let .customChatContents(customChatContents) = interfaceState.subject { - switch customChatContents.kind { - default: - break - } - } - } - actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) - } - - let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - actionButtonsSize.width + 1 - UIScreenPixel + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight), size: actionButtonsSize) - transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) - if let (rect, containerSize) = self.absoluteRect { - self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition) - } - - let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize) - transition.updateFrame(node: self.slowModeButton, frame: slowModeButtonFrame) - - if let _ = interfaceState.inputTextPanelState.mediaRecordingState { - let text: String = interfaceState.strings.VoiceOver_MessageContextSend - let mediaRecordingAccessibilityArea: AccessibilityAreaNode - var added = false - if let current = self.mediaRecordingAccessibilityArea { - mediaRecordingAccessibilityArea = current - } else { - added = true - mediaRecordingAccessibilityArea = AccessibilityAreaNode() - mediaRecordingAccessibilityArea.accessibilityLabel = text - mediaRecordingAccessibilityArea.accessibilityTraits = [.button, .startsMediaSession] - self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea - mediaRecordingAccessibilityArea.activate = { [weak self] in - if let self { - self.interfaceInteraction?.finishMediaRecording(.send(viewOnce: self.viewOnce)) - } - return true - } - self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) - } - self.actionButtons.isAccessibilityElement = false - let size: CGFloat = 120.0 - mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: actionButtonsFrame.midX - size / 2.0, y: actionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size)) - if added { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4, execute: { - [weak mediaRecordingAccessibilityArea] in - UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: mediaRecordingAccessibilityArea?.view) - }) - } - } else { - self.actionButtons.isAccessibilityElement = true - if let mediaRecordingAccessibilityArea = self.mediaRecordingAccessibilityArea { - self.mediaRecordingAccessibilityArea = nil - mediaRecordingAccessibilityArea.removeFromSupernode() - } - } - - let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) - var textFieldInsets = self.textFieldInsets(metrics: metrics) - if actionButtonsSize.width > 44.0 { - textFieldInsets.right = actionButtonsSize.width - 2.0 - } - if additionalSideInsets.right > 0.0 { - textFieldInsets.right += additionalSideInsets.right / 3.0 - } - self.actionButtons.micButton.isHidden = additionalSideInsets.right > 0.0 - - transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) - if let image = self.searchLayoutClearImageNode.image { - self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size) - } - - var textInputViewRealInsets = UIEdgeInsets() - if let presentationInterfaceState = self.presentationInterfaceState { - textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) - } - - let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) - transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) - transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) - transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) - - if let textInputNode = self.textInputNode { - textInputNode.textContainerInset = textInputViewRealInsets - let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) - let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size - //transition.updateFrame(node: textInputNode, frame: textFieldFrame) - textInputNode.frame = textFieldFrame - textInputNode.updateLayout(size: textFieldFrame.size) - self.updateInputField(textInputFrame: textFieldFrame, transition: ComponentTransition(transition)) - if shouldUpdateLayout { - textInputNode.layout() - } - } - - if interfaceState.slowmodeState == nil || isScheduledMessages, let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder { - let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let contextPlaceholderNode = placeholderApply() - if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode { - self.contextPlaceholderNode = nil - currentContextPlaceholderNode.removeFromSupernode() - } - - if self.contextPlaceholderNode !== contextPlaceholderNode { - contextPlaceholderNode.displaysAsynchronously = false - contextPlaceholderNode.isUserInteractionEnabled = false - self.contextPlaceholderNode = contextPlaceholderNode - self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) - } - - let _ = placeholderApply() - - let placeholderTransition: ContainedViewLayoutTransition - if placeholderSize.size.width == contextPlaceholderNode.frame.width { - placeholderTransition = transition - } else { - placeholderTransition = .immediate - } - placeholderTransition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size)) - contextPlaceholderNode.alpha = audioRecordingItemsAlpha - } else if let contextPlaceholderNode = self.contextPlaceholderNode { - self.contextPlaceholderNode = nil - contextPlaceholderNode.removeFromSupernode() - self.textPlaceholderNode.alpha = 1.0 - } - - if let slowmodeState = interfaceState.slowmodeState, !isScheduledMessages && rightSlowModeInset.isZero { - let slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode - if let current = self.slowmodePlaceholderNode { - slowmodePlaceholderNode = current - } else { - slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme) - self.slowmodePlaceholderNode = slowmodePlaceholderNode - self.clippingNode.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode) - } - let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0)) - slowmodePlaceholderNode.updateState(slowmodeState) - slowmodePlaceholderNode.frame = placeholderFrame - slowmodePlaceholderNode.alpha = audioRecordingItemsAlpha - slowmodePlaceholderNode.updateLayout(size: placeholderFrame.size) - } else if let slowmodePlaceholderNode = self.slowmodePlaceholderNode { - self.slowmodePlaceholderNode = nil - slowmodePlaceholderNode.removeFromSupernode() - } - - if (interfaceState.slowmodeState != nil && rightSlowModeInset.isZero && !isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil { - self.textPlaceholderNode.isHidden = true - self.slowmodePlaceholderNode?.isHidden = inputHasText - } else { - self.textPlaceholderNode.isHidden = inputHasText - self.slowmodePlaceholderNode?.isHidden = true - } - - var nextButtonTopRight = CGPoint(x: hideOffset.x + width - rightInset - textFieldInsets.right - accessoryButtonInset - rightSlowModeInset, y: hideOffset.y + panelHeight - textFieldInsets.bottom - minimalInputHeight) - for (item, button) in self.accessoryItemButtons.reversed() { - let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) - button.updateLayout(item: item, size: buttonSize) - let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) - if button.supernode == nil { - self.clippingNode.addSubnode(button) - button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0) - transition.updateFrame(layer: button.layer, frame: buttonFrame) - if animatedTransition { - button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - button.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) - } - } else { - transition.updateFrame(layer: button.layer, frame: buttonFrame) - } - nextButtonTopRight.x -= buttonSize.width - nextButtonTopRight.x -= accessoryButtonSpacing - } - - let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) - self.currentTextInputBackgroundWidthOffset = textInputBackgroundWidthOffset - transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame) - transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha) - - let textPlaceholderSize: CGSize - let textPlaceholderMaxWidth: CGFloat = max(1.0, (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 12.0) - - if (updatedPlaceholder != nil && self.currentPlaceholder != updatedPlaceholder) || themeUpdated { - let currentPlaceholder = updatedPlaceholder ?? self.currentPlaceholder ?? "" - self.currentPlaceholder = currentPlaceholder - let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) - - let attributedPlaceholder = NSMutableAttributedString(string: currentPlaceholder, font:Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) - if placeholderHasStar, let range = attributedPlaceholder.string.range(of: "#") { - attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) - attributedPlaceholder.addAttribute(.foregroundColor, value: interfaceState.theme.chat.inputPanel.inputPlaceholderColor, range: NSRange(range, in: attributedPlaceholder.string)) - attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) - } - - self.textPlaceholderNode.attributedText = attributedPlaceholder - self.textInputNode?.textView.accessibilityHint = currentPlaceholder - let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) - if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { - self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer) - snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in - snapshotLayer?.removeFromSuperlayer() - }) - self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) - } - textPlaceholderSize = placeholderSize - } else { - textPlaceholderSize = self.textPlaceholderNode.bounds.size - } - - let textPlaceholderFrame: CGRect - if sendingTextDisabled { - textPlaceholderFrame = CGRect(origin: CGPoint(x: textInputBackgroundFrame.minX + floor((textInputBackgroundFrame.width - textPlaceholderSize.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: textPlaceholderSize) - - let textLockIconNode: ASImageNode - var textLockIconTransition = transition - if let current = self.textLockIconNode { - textLockIconNode = current - } else { - textLockIconTransition = .immediate - textLockIconNode = ASImageNode() - self.textLockIconNode = textLockIconNode - self.textPlaceholderNode.addSubnode(textLockIconNode) - - textLockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) - } - - if let image = textLockIconNode.image { - textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size)) - } - } else { - textPlaceholderFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: textPlaceholderSize) - - if let textLockIconNode = self.textLockIconNode { - self.textLockIconNode = nil - textLockIconNode.removeFromSupernode() - } - } - transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame) - - let textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha - transition.updateAlpha(node: self.textPlaceholderNode, alpha: textPlaceholderAlpha) - - if let removeAccessoryButtons = removeAccessoryButtons { - for button in removeAccessoryButtons { - let buttonFrame = CGRect(origin: CGPoint(x: button.frame.origin.x + additionalOffset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight), size: button.frame.size) - transition.updateFrame(layer: button.layer, frame: buttonFrame) - button.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak button] _ in - button?.removeFromSupernode() - }) - } - } let mediaInputDisabled: Bool if !interfaceState.voiceMessagesAvailable { @@ -2961,11 +3188,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if case let .media(_, _, focused) = interfaceState.inputMode, focused { clippingDelta = -panelHeight } - transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight))) transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta)) let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) - let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 40.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) @@ -2980,7 +3207,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.viewOnceButton.isHidden = true } - return panelHeight + return contentHeight } @objc private func slowModeButtonPressed() { @@ -3503,10 +3730,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let (width, leftInset, rightInset, _, _, maxHeight, metrics, _, _) = self.validLayout { var composeButtonsOffset: CGFloat = 0.0 if self.extendedSearchLayout { - composeButtonsOffset = 44.0 + composeButtonsOffset = 40.0 } - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics) + let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) var textFieldMinHeight: CGFloat = 33.0 if let presentationInterfaceState = self.presentationInterfaceState { @@ -3514,7 +3741,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } let minimalHeight: CGFloat = 14.0 + textFieldMinHeight - let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0)) + let counterSize = self.counterTextNode.updateLayout(CGSize(width: 40.0, height: 40.0)) let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height)) transition.updateFrame(node: self.counterTextNode, frame: counterFrame) @@ -3804,15 +4031,16 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let interfaceState = self.presentationInterfaceState { if (interfaceState.slowmodeState != nil && !isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil { self.textPlaceholderNode.isHidden = true + self.tintMaskTextPlaceholderNode.isHidden = true self.slowmodePlaceholderNode?.isHidden = inputHasText } else { self.textPlaceholderNode.isHidden = inputHasText + self.tintMaskTextPlaceholderNode.isHidden = inputHasText self.slowmodePlaceholderNode?.isHidden = true } } let _ = hideMicButton -// self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: animated) self.updateTextHeight(animated: animated) } @@ -3969,17 +4197,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if animated { self.actionButtons.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } + + self.actionButtons.micButtonBackgroundView.alpha = 0.0 + if animated { + self.actionButtons.micButtonBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } } } else { let micAlpha: CGFloat = self.actionButtons.micButton.fadeDisabled ? 0.5 : 1.0 if !self.actionButtons.micButton.alpha.isEqual(to: micAlpha) { self.actionButtons.micButton.alpha = micAlpha + self.actionButtons.micButtonBackgroundView.alpha = micAlpha if animated { self.actionButtons.micButton.layer.animateAlpha(from: 0.0, to: micAlpha, duration: 0.1) + self.actionButtons.micButtonBackgroundView.layer.animateAlpha(from: 0.0, to: micAlpha, duration: 0.1) if animateWithBounce { self.actionButtons.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.actionButtons.micButtonBackgroundView.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) } else { self.actionButtons.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + self.actionButtons.micButtonBackgroundView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) } } } @@ -4011,7 +4248,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private func updateTextHeight(animated: Bool) { if let (width, leftInset, rightInset, _, additionalSideInsets, maxHeight, metrics, _, _) = self.validLayout { - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics) + let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if !self.bounds.size.height.isEqual(to: panelHeight) { self.updateHeight(animated) @@ -4803,6 +5040,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) } + for (_, button) in self.accessoryItemButtons { + if let result = button.hitTest(self.view.convert(point, to: button), with: event) { + return result + } + } + let result = super.hitTest(point, with: event) return result } @@ -4869,14 +5112,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } func makeSnapshotForTransition() -> ChatMessageTransitionNodeImpl.Source.TextInput? { - guard let backgroundImage = self.transparentTextInputBackgroundImage else { - return nil - } guard let textInputNode = self.textInputNode else { return nil } - let backgroundView = UIImageView(image: backgroundImage) + //TODO + let backgroundView = UIImageView() backgroundView.frame = self.textInputBackgroundNode.frame let caretColor = textInputNode.textView.tintColor diff --git a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift index 75b0164c34..2632c95b98 100644 --- a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift @@ -222,7 +222,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { return } self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() - self.interfaceInteraction?.presentLinkOptions(self) + self.interfaceInteraction?.presentLinkOptions(self.view) Queue.mainQueue().after(1.5) { self.updateThemeAndStrings(theme: self.theme, strings: self.strings, force: true) }