Merge branch 'master' into glass

# Conflicts:
#	submodules/AttachmentUI/Sources/AttachmentPanel.swift
#	submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift
#	submodules/TelegramUI/BUILD
#	submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift
#	submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift
#	submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift
#	submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
#	submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift
#	submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift
#	submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift
This commit is contained in:
Isaac 2025-09-02 19:55:51 +02:00
commit 5de7e74180
45 changed files with 2469 additions and 2192 deletions

View file

@ -1096,6 +1096,8 @@ public protocol ChatController: ViewController {
func playShakeAnimation()
func removeAd(opaqueId: Data)
func restrictedSendingContentsText() -> String
}
public protocol ChatMessagePreviewItemNode: AnyObject {

View file

@ -1374,7 +1374,7 @@ public class AttachmentController: ViewController, MinimizableController {
let inputNode: ASDisplayNode
let accessoryPanelNode: ASDisplayNode?
let menuButtonNode: ASDisplayNode
let menuButtonBackgroundNode: ASDisplayNode
let menuButtonBackgroundView: UIView
let menuIconNode: ASDisplayNode
let menuTextNode: ASDisplayNode
let prepareForDismiss: () -> Void
@ -1383,7 +1383,7 @@ public class AttachmentController: ViewController, MinimizableController {
inputNode: ASDisplayNode,
accessoryPanelNode: ASDisplayNode?,
menuButtonNode: ASDisplayNode,
menuButtonBackgroundNode: ASDisplayNode,
menuButtonBackgroundView: UIView,
menuIconNode: ASDisplayNode,
menuTextNode: ASDisplayNode,
prepareForDismiss: @escaping () -> Void
@ -1391,7 +1391,7 @@ public class AttachmentController: ViewController, MinimizableController {
self.inputNode = inputNode
self.accessoryPanelNode = accessoryPanelNode
self.menuButtonNode = menuButtonNode
self.menuButtonBackgroundNode = menuButtonBackgroundNode
self.menuButtonBackgroundView = menuButtonBackgroundView
self.menuIconNode = menuIconNode
self.menuTextNode = menuTextNode
self.prepareForDismiss = prepareForDismiss

View file

@ -1276,6 +1276,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}, dismissUrlPreview: {
}, dismissForwardMessages: {
}, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {
@ -1628,7 +1630,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
self.animatingTransition = true
let targetButtonColor = self.mainButtonNode.backgroundColor
self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundNode.backgroundColor
self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundView.backgroundColor
transition.updateBackgroundColor(node: self.mainButtonNode, color: targetButtonColor ?? .clear)
transition.animateFrame(layer: self.mainButtonNode.layer, from: inputTransition.menuButtonNode.frame)
@ -1686,7 +1688,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}
let sourceButtonColor = self.mainButtonNode.backgroundColor
transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundNode.backgroundColor ?? .clear)
transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundView.backgroundColor ?? .clear)
let sourceButtonFrame = self.mainButtonNode.frame
transition.updateFrame(node: self.mainButtonNode, frame: inputTransition.menuButtonNode.frame)

View file

@ -15,6 +15,7 @@ swift_library(
"//submodules/LegacyComponents",
"//submodules/MetalEngine",
"//submodules/TelegramUI/Components/Calls/CallScreen",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",

View file

@ -3,6 +3,7 @@ import UIKit
import AsyncDisplayKit
import Display
import LegacyComponents
import GlassBackgroundComponent
public final class VoiceBlobNode: ASDisplayNode {
public init(
@ -265,6 +266,8 @@ final class BlobNode: ASDisplayNode {
return layer
}()
private var backgroundView: GlassBackgroundView?
private var transition: CGFloat = 0 {
didSet {
guard let currentPoints = currentPoints else { return }
@ -291,6 +294,8 @@ final class BlobNode: ASDisplayNode {
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true
private var color: UIColor?
init(
pointsCount: Int,
minRandomness: CGFloat,
@ -326,6 +331,13 @@ final class BlobNode: ASDisplayNode {
self.layer.addSublayer(self.shapeLayer)
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
if isCircle {
let backgroundView = GlassBackgroundView()
self.backgroundView = backgroundView
self.shapeLayer.removeFromSuperlayer()
self.view.addSubview(backgroundView)
}
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
@ -339,6 +351,8 @@ final class BlobNode: ASDisplayNode {
}
func setColor(_ color: UIColor, animated: Bool) {
self.color = color
let previousColor = self.shapeLayer.fillColor
self.shapeLayer.fillColor = color.cgColor
if animated, let previousColor = previousColor, self.isCurrentlyInHierarchy {
@ -444,6 +458,12 @@ final class BlobNode: ASDisplayNode {
).cgPath
}
CATransaction.commit()
if let backgroundView = self.backgroundView, let color = self.color {
let halfWidth = floor(self.bounds.width * self.minScale)
backgroundView.update(size: CGSize(width: halfWidth, height: halfWidth), cornerRadius: halfWidth * 0.5, isDark: false, tintColor: color, transition: .immediate)
backgroundView.frame = CGRect(origin: CGPoint(x: (self.bounds.width - halfWidth) * 0.5, y: (self.bounds.height - halfWidth) * 0.5), size: CGSize(width: halfWidth, height: halfWidth))
}
}
}

View file

@ -22,6 +22,8 @@ swift_library(
"//submodules/ChatContextQuery",
"//submodules/TooltipUI",
"//submodules/AudioWaveform",
"//submodules/UndoUI",
"//submodules/TextFormat",
],
visibility = [
"//visibility:public",

View file

@ -8,6 +8,8 @@ import Display
import AccountContext
import ContextUI
import TooltipUI
import UndoUI
import TextFormat
public enum ChatLoadingMessageSubject {
case generic
@ -190,6 +192,8 @@ public final class ChatPanelInterfaceInteraction {
public let dismissUrlPreview: () -> Void
public let dismissForwardMessages: () -> Void
public let dismissSuggestPost: () -> Void
public let displayUndo: (UndoOverlayContent) -> Void
public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void
public let requestLayout: (ContainedViewLayoutTransition) -> Void
public let chatController: () -> ViewController?
public let statuses: ChatPanelInterfaceInteractionStatuses?
@ -312,6 +316,8 @@ public final class ChatPanelInterfaceInteraction {
dismissUrlPreview: @escaping () -> Void,
dismissForwardMessages: @escaping () -> Void,
dismissSuggestPost: @escaping () -> Void,
displayUndo: @escaping (UndoOverlayContent) -> Void,
sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void,
updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void,
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
toggleChatSidebarMode: @escaping () -> Void,
@ -437,6 +443,8 @@ public final class ChatPanelInterfaceInteraction {
self.dismissUrlPreview = dismissUrlPreview
self.dismissForwardMessages = dismissForwardMessages
self.dismissSuggestPost = dismissSuggestPost
self.displayUndo = displayUndo
self.sendEmoji = sendEmoji
self.updateHistoryFilter = updateHistoryFilter
self.updateChatLocationThread = updateChatLocationThread
self.toggleChatSidebarMode = toggleChatSidebarMode
@ -571,6 +579,8 @@ public final class ChatPanelInterfaceInteraction {
}, dismissUrlPreview: {
}, dismissForwardMessages: {
}, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View file

@ -201,22 +201,33 @@ public protocol GradientBackgroundPatternOverlayLayer: CALayer {
public final class GradientBackgroundNode: ASDisplayNode {
public final class CloneNode: ASImageNode {
private weak var parentNode: GradientBackgroundNode?
private let isDimmed: Bool
private var index: SparseBag<Weak<CloneNode>>.Index?
public init(parentNode: GradientBackgroundNode) {
public init(parentNode: GradientBackgroundNode, isDimmed: Bool) {
self.parentNode = parentNode
self.isDimmed = isDimmed
super.init()
self.displaysAsynchronously = false
self.index = parentNode.cloneNodes.add(Weak<CloneNode>(self))
self.image = parentNode.dimmedImage
if isDimmed {
self.index = parentNode.cloneNodes.add(Weak<CloneNode>(self))
self.image = parentNode.dimmedImage
} else {
self.index = parentNode.rawCloneNodes.add(Weak<CloneNode>(self))
self.image = parentNode.rawImage
}
}
deinit {
if let parentNode = self.parentNode, let index = self.index {
parentNode.cloneNodes.remove(index)
if self.isDimmed {
parentNode.cloneNodes.remove(index)
} else {
parentNode.rawCloneNodes.remove(index)
}
}
}
}
@ -258,9 +269,14 @@ public final class GradientBackgroundNode: ASDisplayNode {
return nil
}
}
private var rawImage: UIImage? {
return self.contentView.image
}
private var validLayout: CGSize?
private let cloneNodes = SparseBag<Weak<CloneNode>>()
private let rawCloneNodes = SparseBag<Weak<CloneNode>>()
private let useSharedAnimationPhase: Bool
static var sharedPhase: Int = 0
@ -490,6 +506,24 @@ public final class GradientBackgroundNode: ASDisplayNode {
}
}
}
if !self.rawCloneNodes.isEmpty {
let cloneAnimation = CAKeyframeAnimation(keyPath: "contents")
cloneAnimation.values = images.map { $0.0.cgImage! }
cloneAnimation.duration = animation.duration
cloneAnimation.calculationMode = animation.calculationMode
cloneAnimation.isRemovedOnCompletion = animation.isRemovedOnCompletion
cloneAnimation.fillMode = animation.fillMode
cloneAnimation.beginTime = animation.beginTime
for cloneNode in self.rawCloneNodes {
if let value = cloneNode.value {
value.image = images.last?.0
value.layer.removeAnimation(forKey: "contents")
value.layer.add(cloneAnimation, forKey: "contents")
}
}
}
} else {
let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions)
self.contentView.image = image
@ -502,6 +536,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage
}
for cloneNode in self.rawCloneNodes {
cloneNode.value?.image = image
}
completion()
}
@ -516,6 +553,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage
}
for cloneNode in self.rawCloneNodes {
cloneNode.value?.image = image
}
self.validPhase = self.phase
@ -534,6 +574,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage
}
for cloneNode in self.rawCloneNodes {
cloneNode.value?.image = image
}
self.validPhase = self.phase

View file

@ -65,6 +65,12 @@
@end
@protocol TGModernConversationInputMicButtonLockPanelView <NSObject>
- (void)updateSize:(CGSize)size;
@end
@interface TGModernConversationInputMicButton : UIButton
@property (nonatomic, weak) id<TGModernConversationInputMicButtonDelegate> delegate;
@ -88,6 +94,6 @@
- (void)_commitLocked;
- (void)setHidesPanelOnLock;
- (UIView *)createLockPanelView;
- (UIView<TGModernConversationInputMicButtonLockPanelView> *)createLockPanelView;
@end

View file

@ -98,6 +98,17 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
@end
@interface TGModernConversationInputMicButtonLockPanelViewNativeImpl : UIImageView<TGModernConversationInputMicButtonLockPanelView>
@end
@implementation TGModernConversationInputMicButtonLockPanelViewNativeImpl
- (void)updateSize:(CGSize)size {
}
@end
@interface TGModernConversationInputMicButton () <UIGestureRecognizerDelegate>
{
CGPoint _touchLocation;
@ -116,7 +127,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
UIImageView *_innerIconView;
UIView *_lockPanelWrapperView;
UIView *_lockPanelView;
UIView<TGModernConversationInputMicButtonLockPanelView> *_lockPanelView;
UIImageView *_lockArrowView;
TGModernConversationInputLockView *_lockView;
UIImage *_previousIcon;
@ -353,8 +364,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
return stopButtonImage;
}
- (UIView *)createLockPanelView {
UIImageView *view = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)];
- (UIView<TGModernConversationInputMicButtonLockPanelView> *)createLockPanelView {
TGModernConversationInputMicButtonLockPanelViewNativeImpl *view = [[TGModernConversationInputMicButtonLockPanelViewNativeImpl alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)];
view.userInteractionEnabled = true;
view.image = [self panelBackgroundImage];
return view;
@ -634,6 +645,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
[snapshotView removeFromSuperview];
}];
[_lockPanelView updateSize:CGSizeMake(_lockPanelView.frame.size.width, 72.0f - 32.0f)];
[UIView animateWithDuration:0.2 animations:^
{
snapshotView.alpha = 0.0f;

View file

@ -37,7 +37,7 @@ final class CustomPasscodeBackground: PasscodeBackground {
func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? {
if self.inverted, let backgroundNode = backgroundNode as? GradientBackgroundNode {
return GradientBackgroundNode.CloneNode(parentNode: backgroundNode)
return GradientBackgroundNode.CloneNode(parentNode: backgroundNode, isDimmed: true)
} else {
return nil
}

View file

@ -486,10 +486,12 @@ swift_library(
"//submodules/TelegramUI/Components/FaceScanScreen",
"//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode",
"//submodules/ContactsHelper",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel",
"//submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel",
"//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View file

@ -24,7 +24,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
private let backgroundColorNode: ASDisplayNode
private let backgroundAdditionalColorNode: ASDisplayNode
private let shadowNode: ASImageNode
private let highlightNode: ASImageNode
private let textNode: ImmediateTextNode
@ -37,24 +36,17 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
self.backgroundContainerNode.clipsToBounds = true
self.backgroundContainerNode.allowsGroupOpacity = true
self.backgroundContainerNode.isUserInteractionEnabled = false
self.backgroundContainerNode.cornerRadius = 5.0
if #available(iOS 13.0, *) {
self.backgroundContainerNode.layer.cornerCurve = .continuous
}
self.backgroundContainerNode.cornerRadius = 10.0
self.backgroundContainerNode.layer.cornerCurve = .continuous
self.backgroundColorNode = ASDisplayNode()
self.backgroundColorNode.cornerRadius = 5.0
if #available(iOS 13.0, *) {
self.backgroundColorNode.layer.cornerCurve = .continuous
}
self.backgroundColorNode.cornerRadius = 10.0
self.backgroundColorNode.layer.cornerCurve = .continuous
self.backgroundAdditionalColorNode = ASDisplayNode()
self.backgroundAdditionalColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
self.backgroundAdditionalColorNode.isHidden = true
self.shadowNode = ASImageNode()
self.shadowNode.isUserInteractionEnabled = false
self.highlightNode = ASImageNode()
self.highlightNode.isUserInteractionEnabled = false
@ -73,7 +65,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
self.backgroundContainerNode.addSubnode(self.backgroundAdditionalColorNode)
self.addSubnode(self.textNode)
self.backgroundContainerNode.addSubnode(self.shadowNode)
self.backgroundContainerNode.addSubnode(self.highlightNode)
self.highligthedChanged = { [weak self] highlighted in
@ -146,7 +137,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
self.theme = theme
self.highlightNode.image = PresentationResourcesChat.chatInputButtonPanelButtonHighlightImage(theme)
self.shadowNode.image = PresentationResourcesChat.chatInputButtonPanelButtonShadowImage(theme)
self.updateIcon()
}
@ -182,7 +172,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
self.backgroundNode?.frame = self.backgroundColorNode.frame
self.highlightNode.frame = self.bounds
self.shadowNode.frame = self.bounds
if let (rect, containerSize) = self.absoluteRect {
self.update(rect: rect, within: containerSize, transition: .immediate)
@ -201,7 +190,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
private let context: AccountContext
private let controllerInteraction: ChatControllerInteraction
private let separatorNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var backgroundNode: WallpaperBubbleBackgroundNode?
@ -220,9 +208,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
self.backgroundColorNode = ASDisplayNode()
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.backgroundColorNode)
@ -232,8 +217,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.alwaysBounceHorizontal = false
self.scrollNode.view.alwaysBounceVertical = false
self.addSubnode(self.separatorNode)
}
override public func didLoad() {
@ -264,8 +247,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
}
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel)))
if self.backgroundNode == nil {
if let backgroundNode = self.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
self.backgroundNode = backgroundNode
@ -277,7 +258,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
if updatedTheme {
self.theme = interfaceState.theme
self.separatorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelSeparatorColor
self.backgroundColorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelBackgroundColor
}
@ -296,8 +276,8 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
self.message = interfaceState.keyboardButtonsMessage
if let markup = validatedMarkup {
let verticalInset: CGFloat = 10.0
let sideInset: CGFloat = 6.0 + leftInset
let verticalInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + leftInset
var buttonHeight: CGFloat = 43.0
let columnSpacing: CGFloat = 6.0
let rowSpacing: CGFloat = 5.0

View file

@ -176,6 +176,8 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, dismissUrlPreview: {
}, dismissForwardMessages: {
}, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View file

@ -0,0 +1,40 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatRecordingPreviewInputPanelNode",
module_name = "ChatRecordingPreviewInputPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/AppBundle",
"//submodules/ContextUI",
"//submodules/AnimationUI",
"//submodules/ManagedAnimationNode",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
"//submodules/TelegramUI/Components/AudioWaveformNode",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TooltipUI",
"//submodules/TelegramNotices",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/MediaScrubberComponent",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,721 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import UniversalMediaPlayer
import AppBundle
import ContextUI
import AnimationUI
import ManagedAnimationNode
import ChatPresentationInterfaceState
import ChatSendButtonRadialStatusNode
import AudioWaveformNode
import ChatInputPanelNode
import TooltipUI
import TelegramNotices
import ComponentFlow
import MediaScrubberComponent
import AnimatedCountLabelNode
import ChatRecordingViewOnceButtonNode
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
#if SWIFT_PACKAGE
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
}
#else
extension AudioWaveformNode: @retroactive CustomMediaPlayerScrubbingForegroundNode {
}
#endif
final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
let ignoreHit: (UIView, CGPoint) -> Bool
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool) {
self.ignoreHit = ignoreHit
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func maybeDismissContent(point: CGPoint) {
for subview in self.subviews.reversed() {
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
return
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
if event == nil || self.ignoreHit(self, point) {
return nil
}
return nil
}
}
final class PlayButtonNode: ASDisplayNode {
let backgroundView: GlassBackgroundView
let playButton: HighlightableButtonNode
fileprivate let playPauseIconNode: PlayPauseIconNode
let durationLabel: MediaPlayerTimeTextNode
var pressed: () -> Void = {}
init(theme: PresentationTheme) {
self.backgroundView = GlassBackgroundView(frame: CGRect())
self.playButton = HighlightableButtonNode()
self.playButton.displaysAsynchronously = false
self.playPauseIconNode = PlayPauseIconNode()
self.playPauseIconNode.enqueueState(.play, animated: false)
self.playPauseIconNode.customColor = theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.7)
self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.7), textFont: Font.with(size: 13.0, weight: .semibold, traits: .monospacedNumbers))
self.durationLabel.alignment = .right
self.durationLabel.mode = .normal
self.durationLabel.showDurationIfNotStarted = true
super.init()
self.view.addSubview(self.backgroundView)
self.addSubnode(self.playButton)
self.backgroundView.contentView.addSubview(self.playPauseIconNode.view)
self.backgroundView.contentView.addSubview(self.durationLabel.view)
self.playButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.backgroundView.frame.contains(point)
}
@objc private func buttonPressed() {
self.pressed()
}
func update(theme: PresentationTheme, size: CGSize, transition: ContainedViewLayoutTransition) {
var buttonSize = CGSize(width: 63.0, height: 22.0)
if size.width < 70.0 {
buttonSize.width = 27.0
}
let backgroundFrame = buttonSize.centered(in: CGRect(origin: .zero, size: size))
transition.updateFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.4), transition: ComponentTransition(transition))
self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 1.0 - UIScreenPixel), size: CGSize(width: 21.0, height: 21.0))
transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: 18.0, y: 3.0), size: CGSize(width: 35.0, height: 20.0)))
transition.updateAlpha(node: self.durationLabel, alpha: buttonSize.width > 27.0 ? 1.0 : 0.0)
self.playButton.frame = CGRect(origin: .zero, size: size)
}
}
private final class ClippedWaveformNode: ASDisplayNode, CustomMediaPlayerScrubbingForegroundNode {
let waveformNode: AudioWaveformNode
let waveformLeftMaskView: UIImageView
let waveformRightMaskView: UIImageView
let waveformMaskView: UIView
let foregroundClippingContainer: ASDisplayNode
let foregroundWaveformNode: AudioWaveformNode
var progress: CGFloat? {
didSet {
if self.progress != oldValue {
self.waveformNode.progress = self.progress
self.foregroundWaveformNode.progress = self.progress
}
}
}
override var frame: CGRect {
didSet {
self.updateLayout()
}
}
override var bounds: CGRect {
didSet {
self.updateLayout()
}
}
override init() {
self.waveformNode = AudioWaveformNode()
self.waveformMaskView = UIView()
self.waveformLeftMaskView = UIImageView()
self.waveformLeftMaskView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
self.waveformLeftMaskView.backgroundColor = .white
self.waveformMaskView.addSubview(self.waveformLeftMaskView)
self.waveformRightMaskView = UIImageView()
self.waveformRightMaskView.layer.anchorPoint = CGPoint()
self.waveformRightMaskView.backgroundColor = .white
self.waveformMaskView.addSubview(self.waveformRightMaskView)
self.foregroundClippingContainer = ASDisplayNode()
self.foregroundClippingContainer.clipsToBounds = true
self.foregroundClippingContainer.anchorPoint = CGPoint()
self.foregroundWaveformNode = AudioWaveformNode()
self.foregroundWaveformNode.isLayerBacked = true
self.foregroundClippingContainer.addSubnode(self.foregroundWaveformNode)
super.init()
self.addSubnode(self.waveformNode)
self.waveformNode.view.mask = self.waveformMaskView
self.addSubnode(self.foregroundClippingContainer)
}
private func updateLayout() {
self.waveformNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.foregroundWaveformNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.waveformLeftMaskView.bounds = CGRect(origin: CGPoint(), size: self.bounds.size)
self.waveformRightMaskView.bounds = CGRect(origin: CGPoint(), size: self.bounds.size)
}
func updateClipping(minX: CGFloat, maxX: CGFloat, transition: ContainedViewLayoutTransition) {
let clippingFrame = CGRect(origin: CGPoint(x: minX, y: 0.0), size: CGSize(width: max(0.0, maxX - minX), height: 40.0 - 2.0 * 2.0))
transition.updatePosition(node: self.foregroundClippingContainer, position: clippingFrame.origin)
transition.updateBounds(node: self.foregroundClippingContainer, bounds: CGRect(origin: CGPoint(x: minX, y: 0.0), size: clippingFrame.size))
transition.updatePosition(layer: self.waveformLeftMaskView.layer, position: CGPoint(x: minX, y: 0.0))
transition.updatePosition(layer: self.waveformRightMaskView.layer, position: CGPoint(x: maxX, y: 0.0))
}
}
public final class ChatRecordingPreviewInputPanelNodeImpl: ChatInputPanelNode {
private let waveformButton: ASButtonNode
let waveformBackgroundNodeImpl: ASImageNode
var waveformBackgroundNode: ASDisplayNode {
return self.waveformBackgroundNodeImpl
}
let trimViewImpl: TrimView
var trimView: UIView {
return self.trimViewImpl
}
let playButtonNodeImpl: PlayButtonNode
var playButtonNode: ASDisplayNode {
return self.playButtonNodeImpl
}
let scrubber = ComponentView<Empty>()
private let waveformNode: ClippedWaveformNode
private let tintWaveformNode: AudioWaveformNode
private let waveformForegroundNode: AudioWaveformNode
let waveformScrubberNodeImpl: MediaPlayerScrubbingNode
var waveformScrubberNode: ASDisplayNode {
return self.waveformScrubberNodeImpl
}
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var mediaPlayer: MediaPlayer?
private var statusValue: MediaPlayerStatus?
private let statusDisposable = MetaDisposable()
private var scrubbingDisposable: Disposable?
private var positionTimer: SwiftSignalKit.Timer?
private(set) var gestureRecognizer: ContextGesture?
public let tintMaskView: UIView = UIView()
public init(theme: PresentationTheme) {
self.waveformBackgroundNodeImpl = ASImageNode()
self.waveformBackgroundNodeImpl.isLayerBacked = true
self.waveformBackgroundNodeImpl.displaysAsynchronously = false
self.waveformBackgroundNodeImpl.displayWithoutProcessing = true
self.waveformBackgroundNodeImpl.image = generateStretchableFilledCircleImage(diameter: 40.0 - 2.0 * 2.0, color: theme.list.itemCheckColors.fillColor)
self.waveformButton = ASButtonNode()
self.waveformButton.accessibilityTraits.insert(.startsMediaSession)
self.waveformNode = ClippedWaveformNode()
self.waveformForegroundNode = AudioWaveformNode()
self.waveformForegroundNode.isLayerBacked = true
self.tintWaveformNode = AudioWaveformNode()
self.tintWaveformNode.isLayerBacked = true
self.waveformScrubberNodeImpl = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode))
self.trimViewImpl = TrimView(frame: .zero)
self.trimViewImpl.isHollow = true
self.playButtonNodeImpl = PlayButtonNode(theme: theme)
super.init()
self.tintMaskView.layer.addSublayer(self.tintWaveformNode.layer)
self.viewForOverlayContent = ChatRecordingPreviewViewForOverlayContent(
ignoreHit: { [weak self] view, point in
guard let strongSelf = self else {
return false
}
if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil {
return true
}
if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY {
return true
}
return false
}
)
self.addSubnode(self.waveformBackgroundNodeImpl)
self.addSubnode(self.waveformScrubberNode)
//self.addSubnode(self.waveformButton)
self.view.addSubview(self.trimViewImpl)
self.addSubnode(self.playButtonNodeImpl)
self.playButtonNodeImpl.pressed = { [weak self] in
guard let self else {
return
}
self.waveformPressed()
}
self.waveformScrubberNodeImpl.seek = { [weak self] timestamp in
guard let self else {
return
}
var timestamp = timestamp
if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange {
timestamp = max(trimRange.lowerBound, min(timestamp, trimRange.upperBound))
}
self.mediaPlayer?.seek(timestamp: timestamp)
}
self.scrubbingDisposable = (self.waveformScrubberNodeImpl.scrubbingPosition
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(node: self.playButtonNodeImpl, alpha: value != nil ? 0.0 : 1.0)
})
self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside)
}
deinit {
self.mediaPlayer?.pause()
self.statusDisposable.dispose()
self.scrubbingDisposable?.dispose()
self.positionTimer?.invalidate()
}
override public func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
private func ensureHasTimer() {
if self.positionTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.checkPosition()
}, queue: Queue.mainQueue())
self.positionTimer = timer
timer.start()
}
}
func checkPosition() {
guard let statusValue = self.statusValue, let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange, let mediaPlayer = self.mediaPlayer else {
return
}
let timestampSeconds: Double
if !statusValue.generationTimestamp.isZero {
timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp)
} else {
timestampSeconds = statusValue.timestamp
}
if timestampSeconds >= trimRange.upperBound {
mediaPlayer.seek(timestamp: trimRange.lowerBound, play: false)
}
}
private func stopTimer() {
self.positionTimer?.invalidate()
self.positionTimer = nil
}
private func maybePresentViewOnceTooltip() {
/*guard let context = self.context else {
return
}
let _ = (ApplicationSpecificNotice.getVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] counter in
guard let self, let interfaceState = self.presentationInterfaceState else {
return
}
if counter >= 3 {
return
}
Queue.mainQueue().after(0.3) {
self.displayViewOnceTooltip(text: interfaceState.strings.Chat_TapToPlayVoiceMessageOnceTooltip, hasIcon: true)
}
let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
})*/
}
override public 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 innerSize = CGSize(width: 40.0, height: 40.0)
let waveformBackgroundFrame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: width - 2.0 * 2.0, height: 40.0 - 2.0 * 2.0))
if self.presentationInterfaceState != interfaceState {
var updateWaveform = false
if self.presentationInterfaceState?.interfaceState.mediaDraftState != interfaceState.interfaceState.mediaDraftState {
updateWaveform = true
}
if self.presentationInterfaceState?.strings !== interfaceState.strings {
self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage
}
self.presentationInterfaceState = interfaceState
if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, let context = self.context {
switch recordedMediaPreview {
case let .audio(audio):
self.waveformButton.isHidden = false
self.waveformBackgroundNodeImpl.isHidden = false
self.waveformForegroundNode.isHidden = false
self.waveformScrubberNodeImpl.isHidden = false
self.playButtonNodeImpl.isHidden = false
if let view = self.scrubber.view, view.superview != nil {
view.removeFromSuperview()
}
if updateWaveform {
self.waveformNode.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.inputControlColor.withMultipliedAlpha(0.4), gravity: .center, waveform: audio.waveform)
self.waveformNode.foregroundWaveformNode.setup(color: interfaceState.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), gravity: .center, waveform: audio.waveform)
self.tintWaveformNode.setup(color: UIColor(white: 0.0, alpha: 0.5), gravity: .center, waveform: audio.waveform)
self.waveformForegroundNode.setup(color: interfaceState.theme.list.itemCheckColors.foregroundColor, gravity: .center, waveform: audio.waveform)
if self.mediaPlayer != nil {
self.mediaPlayer?.pause()
}
let mediaManager = context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: .standalone(resource: audio.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true)
mediaPlayer.actionAtEnd = .action { [weak self] in
guard let self else {
return
}
Queue.mainQueue().async {
guard let interfaceState = self.presentationInterfaceState else {
return
}
var timestamp: Double = 0.0
if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange {
timestamp = trimRange.lowerBound
}
self.mediaPlayer?.seek(timestamp: timestamp, play: false)
}
}
self.mediaPlayer = mediaPlayer
self.playButtonNodeImpl.durationLabel.defaultDuration = Double(audio.duration)
self.playButtonNodeImpl.durationLabel.status = mediaPlayer.status
self.playButtonNodeImpl.durationLabel.trimRange = audio.trimRange
self.waveformScrubberNodeImpl.status = mediaPlayer.status
self.statusDisposable.set((mediaPlayer.status
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
if let self {
switch status.status {
case .playing, .buffering(_, true, _, _):
self.statusValue = status
if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let _ = audio.trimRange {
self.ensureHasTimer()
}
self.playButtonNodeImpl.playPauseIconNode.enqueueState(.pause, animated: true)
default:
self.statusValue = nil
self.stopTimer()
self.playButtonNodeImpl.playPauseIconNode.enqueueState(.play, animated: true)
}
}
}))
}
let minDuration = max(1.0, 56.0 * audio.duration / waveformBackgroundFrame.size.width)
let (leftHandleFrame, rightHandleFrame) = self.trimViewImpl.update(
style: .voiceMessage,
theme: interfaceState.theme,
visualInsets: .zero,
scrubberSize: waveformBackgroundFrame.size,
duration: audio.duration,
startPosition: audio.trimRange?.lowerBound ?? 0.0,
endPosition: audio.trimRange?.upperBound ?? Double(audio.duration),
position: 0.0,
minDuration: minDuration,
maxDuration: Double(audio.duration),
transition: .immediate
)
let waveformForegroundFrame = CGRect(origin: CGPoint(x: 2.0 + leftHandleFrame.minX, y: 2.0), size: CGSize(width: rightHandleFrame.maxX - leftHandleFrame.minX, height: 40.0 - 2.0 * 2.0))
transition.updateFrame(node: self.waveformBackgroundNodeImpl, frame: waveformForegroundFrame)
self.waveformNode.updateClipping(minX: leftHandleFrame.minX - 19.0, maxX: rightHandleFrame.maxX - 19.0, transition: transition)
self.trimViewImpl.trimUpdated = { [weak self] start, end, updatedEnd, apply in
if let self {
self.mediaPlayer?.pause()
self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply)
if apply {
if !updatedEnd {
self.mediaPlayer?.seek(timestamp: start, play: true)
} else {
self.mediaPlayer?.seek(timestamp: max(0.0, end - 1.0), play: true)
}
self.playButtonNodeImpl.durationLabel.isScrubbing = false
Queue.mainQueue().after(0.1) {
self.waveformForegroundNode.alpha = 1.0
}
} else {
self.playButtonNodeImpl.durationLabel.isScrubbing = true
self.waveformForegroundNode.alpha = 0.0
}
let startFraction = start / Double(audio.duration)
let endFraction = end / Double(audio.duration)
self.waveformForegroundNode.trimRange = startFraction ..< endFraction
}
}
self.trimViewImpl.frame = waveformBackgroundFrame
self.trimViewImpl.isHidden = audio.duration < 2.0
let playButtonSize = CGSize(width: max(0.0, rightHandleFrame.minX - leftHandleFrame.maxX), height: waveformBackgroundFrame.height)
self.playButtonNodeImpl.update(theme: interfaceState.theme, size: playButtonSize, transition: transition)
transition.updateFrame(node: self.playButtonNodeImpl, frame: CGRect(origin: CGPoint(x: waveformBackgroundFrame.minX + leftHandleFrame.maxX, y: waveformBackgroundFrame.minY), size: playButtonSize))
case let .video(video):
self.waveformButton.isHidden = true
self.waveformBackgroundNodeImpl.isHidden = true
self.waveformForegroundNode.isHidden = true
self.waveformScrubberNodeImpl.isHidden = true
self.playButtonNodeImpl.isHidden = true
let scrubberSize = self.scrubber.update(
transition: .immediate,
component: AnyComponent(
MediaScrubberComponent(
context: context,
style: .videoMessage,
theme: interfaceState.theme,
generationTimestamp: 0,
position: 0,
minDuration: 1.0,
maxDuration: 60.0,
isPlaying: false,
tracks: [
MediaScrubberComponent.Track(
id: 0,
content: .video(frames: video.frames, framesUpdateTimestamp: video.framesUpdateTimestamp),
duration: Double(video.duration),
trimRange: video.trimRange,
offset: nil,
isMain: true
)
],
isCollage: false,
positionUpdated: { _, _ in },
trackTrimUpdated: { [weak self] _, start, end, updatedEnd, apply in
if let self {
self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply)
}
},
trackOffsetUpdated: { _, _, _ in },
trackLongPressed: { _, _ in }
)
),
environment: {},
forceUpdate: false,
containerSize: CGSize(width: min(424.0, width - leftInset - rightInset - innerSize.width - 1.0), height: 40.0)
)
if let view = self.scrubber.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.bounds = CGRect(origin: .zero, size: scrubberSize)
}
}
}
}
if let view = self.scrubber.view {
view.frame = CGRect(origin: CGPoint(x: min(width - innerSize.width - view.bounds.width, max(leftInset + 45.0, floorToScreenPixels((width - view.bounds.width) / 2.0))), y: 7.0 - UIScreenPixel), size: view.bounds.size)
}
let panelHeight = 40.0
transition.updateFrame(node: self.waveformButton, frame: waveformBackgroundFrame)
let waveformScrubberFrame = CGRect(origin: CGPoint(x: 21.0, y: floor((40.0 - 13.0) / 2.0)), size: CGSize(width: width - 21.0 * 2.0, height: 13.0))
transition.updateFrame(node: self.waveformScrubberNodeImpl, frame: waveformScrubberFrame)
transition.updateFrame(node: self.tintWaveformNode, frame: waveformScrubberFrame)
return panelHeight
}
override public func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool {
return false
}
@objc private func deletePressed() {
self.tooltipController?.dismiss()
self.mediaPlayer?.pause()
self.interfaceInteraction?.deleteRecordedMedia()
}
private weak var tooltipController: TooltipScreen?
@objc private func recordMorePressed() {
self.tooltipController?.dismiss()
self.interfaceInteraction?.resumeMediaRecording()
}
/*private func displayViewOnceTooltip(text: String, hasIcon: Bool) {
guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else {
return
}
let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize())
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .markdown(text: text),
balancedTextLayout: true,
constrainWidth: 240.0,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: hasIcon ? .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil) : nil,
location: .point(location, .right),
displayDuration: .default,
inset: 8.0,
cornerRadius: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
parentController.present(tooltipController, in: .current)
}*/
@objc private func waveformPressed() {
guard let mediaPlayer = self.mediaPlayer else {
return
}
if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange {
let _ = (mediaPlayer.status
|> map(Optional.init)
|> timeout(0.3, queue: Queue.mainQueue(), alternate: .single(nil))
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self, let mediaPlayer = self.mediaPlayer else {
return
}
if let status {
if case .playing = status.status {
mediaPlayer.pause()
} else if status.timestamp <= trimRange.lowerBound {
mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true)
} else {
mediaPlayer.play()
}
} else {
mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true)
}
})
} else {
mediaPlayer.togglePlayPause()
}
}
override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
return defaultHeight(metrics: metrics)
}
}
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .pause
init() {
super.init(size: CGSize(width: 21.0, height: 21.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View file

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatRecordingViewOnceButtonNode",
module_name = "ChatRecordingViewOnceButtonNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,110 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import GlassBackgroundComponent
public final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode {
public enum Icon {
case viewOnce
case recordMore
}
private let icon: Icon
private let backgroundView: GlassBackgroundView
private let iconNode: ASImageNode
private var theme: PresentationTheme?
public init(icon: Icon) {
self.icon = icon
self.backgroundView = GlassBackgroundView()
self.backgroundView.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.isUserInteractionEnabled = false
super.init(pointerStyle: .default)
self.view.addSubview(self.backgroundView)
self.addSubnode(self.iconNode)
self.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateTransformScale(node: self, scale: topScale)
} else {
let transition = ContainedViewLayoutTransition.immediate
transition.updateTransformScale(node: self, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
}
private var innerIsSelected = false
public func update(isSelected: Bool, animated: Bool = false) {
guard let theme = self.theme else {
return
}
let updated = self.iconNode.image == nil || self.innerIsSelected != isSelected
self.innerIsSelected = isSelected
if animated, updated && self.iconNode.image != nil, let snapshot = self.iconNode.view.snapshotContentTree() {
self.view.addSubview(snapshot)
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview()
})
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if updated {
if case .viewOnce = self.icon {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor)
}
}
}
public func update(theme: PresentationTheme) -> CGSize {
let size = CGSize(width: 44.0, height: 44.0)
let innerSize = CGSize(width: 40.0, height: 40.0)
if self.theme !== theme {
self.theme = theme
switch self.icon {
case .viewOnce:
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor)
case .recordMore:
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: theme.chat.inputPanel.panelControlAccentColor)
}
}
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - innerSize.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - innerSize.height / 2.0)), size: innerSize)
self.backgroundView.frame = backgroundFrame
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: .immediate)
if let iconImage = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - iconImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - iconImage.size.height / 2.0)), size: iconImage.size)
self.iconNode.frame = iconFrame
}
return size
}
}

View file

@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputActionButtonsNode",
module_name = "ChatTextInputActionButtonsNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/ContextUI",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ChatMessageBackground",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
"//submodules/ChatSendMessageActionUI",
"//submodules/ComponentFlow",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -128,33 +128,34 @@ private final class EffectBadgeView: UIView {
}
}
final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode {
public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode {
private let context: AccountContext
private let presentationContext: ChatPresentationContext?
private let strings: PresentationStrings
let micButtonBackgroundView: GlassBackgroundView
let micButtonTintMaskView: UIImageView
let micButton: ChatTextInputMediaRecordingButton
public let micButtonBackgroundView: GlassBackgroundView
public let micButtonTintMaskView: UIImageView
public let micButton: ChatTextInputMediaRecordingButton
let sendContainerNode: ASDisplayNode
let sendButtonBackgroundView: GlassBackgroundView
let sendButton: HighlightTrackingButtonNode
var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
var sendButtonHasApplyIcon = false
var animatingSendButton = false
public let sendContainerNode: ASDisplayNode
public let sendButtonBackgroundView: GlassBackgroundView
public let sendButton: HighlightTrackingButtonNode
public var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
public var sendButtonHasApplyIcon = false
public var animatingSendButton = false
let textNode: ImmediateAnimatedCountLabelNode
public let textNode: ImmediateAnimatedCountLabelNode
let expandMediaInputButton: HighlightTrackingButton
public let expandMediaInputButton: HighlightTrackingButton
private let expandMediaInputButtonBackgroundView: GlassBackgroundView
private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView
private var effectBadgeView: EffectBadgeView?
var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)?
public var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)?
private var gestureRecognizer: ContextGesture?
var sendButtonLongPressEnabled = false {
public var sendButtonLongPressEnabled = false {
didSet {
self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled
}
@ -165,7 +166,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
private var validLayout: CGSize?
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
self.context = context
self.presentationContext = presentationContext
let theme = presentationInterfaceState.theme
@ -244,7 +245,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
}
}
override func didLoad() {
override public func didLoad() {
super.didLoad()
let gestureRecognizer = ContextGesture(target: nil, action: nil)
@ -261,13 +262,13 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.sendButtonBackgroundView, style: .lift)
}
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
public func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
self.micButton.updateTheme(theme: theme)
self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor
}
private var absoluteRect: (CGRect, CGSize)?
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
let previousContaierSize = self.absoluteRect?.1
self.absoluteRect = (rect, containerSize)
@ -278,7 +279,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
}
}
func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize {
public func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize {
self.validLayout = size
var innerSize = size
@ -381,7 +382,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
return innerSize
}
func updateAccessibility() {
public func updateAccessibility() {
self.accessibilityTraits = .button
if !self.micButton.alpha.isZero {
switch self.micButton.mode {
@ -398,7 +399,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
}
}
func makeCustomContents() -> UIView? {
public func makeCustomContents() -> UIView? {
if self.sendButtonHasApplyIcon || self.effectBadgeView != nil {
let result = UIView()
result.frame = self.bounds

View file

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputAudioRecordingCancelIndicator",
module_name = "ChatTextInputAudioRecordingCancelIndicator",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -3,32 +3,40 @@ import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import GlassBackgroundComponent
private let cancelFont = Font.regular(17.0)
final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBackgroundView.ContentView {
private let cancel: () -> Void
private let arrowNode: ASImageNode
private let arrowView: GlassBackgroundView.ContentImageView
private let labelNode: TextNode
private let tintLabelNode: TextNode
private let cancelButton: HighlightableButtonNode
private let strings: PresentationStrings
private(set) var isDisplayingCancel = false
public let tintMask: UIView
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
public private(set) var isDisplayingCancel = false
public init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
self.tintMask = UIView()
self.cancel = cancel
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)
self.arrowView = GlassBackgroundView.ContentImageView()
self.arrowView.image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate)
self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor
self.labelNode = TextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.tintLabelNode = TextNode()
self.tintLabelNode.displaysAsynchronously = false
self.tintLabelNode.isUserInteractionEnabled = false
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
self.cancelButton.alpha = 0.0
@ -37,21 +45,28 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
self.strings = strings
super.init()
super.init(frame: CGRect())
self.addSubnode(self.arrowNode)
self.addSubnode(self.labelNode)
self.addSubview(self.arrowView)
self.tintMask.addSubview(self.arrowView.tintMask)
self.addSubview(self.labelNode.view)
self.tintMask.addSubview(self.tintLabelNode.view)
self.addSubnode(self.cancelButton)
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.inputControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()
let _ = tintLabelApply
let arrowSize = self.arrowNode.image?.size ?? CGSize()
let arrowSize = self.arrowView.image?.size ?? CGSize()
let height = max(arrowSize.height, labelLayout.size.height)
self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height))
self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize)
self.arrowView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize)
self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: 1.0 + floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
self.tintLabelNode.frame = self.labelNode.frame
let cancelSize = self.cancelButton.measure(CGSize(width: 200.0, height: 100.0))
self.cancelButton.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - cancelSize.width) / 2.0), y: floor((height - cancelSize.height) / 2.0)), size: cancelSize)
@ -59,19 +74,26 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
}
func updateTheme(theme: PresentationTheme) {
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)
self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (_, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) {
public func updateTheme(theme: PresentationTheme) {
self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor
self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
let makeLayout = TextNode.asyncLayout(self.labelNode)
let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode)
let (_, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.actionControlForegroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()
let _ = tintLabelApply()
}
public func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) {
if self.isDisplayingCancel != isDisplayingCancel {
self.isDisplayingCancel = isDisplayingCancel
if isDisplayingCancel {
self.arrowNode.alpha = 0.0
self.arrowView.alpha = 0.0
self.labelNode.alpha = 0.0
self.cancelButton.alpha = 1.0
@ -85,17 +107,17 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
self.cancelButton.layer.animatePosition(from: CGPoint(x: 0.0, y: 22.0), to: CGPoint(), duration: 0.2, additive: true)
self.cancelButton.layer.animateScale(from: 0.25, to: 1.0, duration: 0.25)
self.arrowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
self.arrowView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
} else {
self.arrowNode.alpha = 1.0
self.arrowView.alpha = 1.0
self.labelNode.alpha = 1.0
self.cancelButton.alpha = 0.0
if animated {
self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.arrowView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.cancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
@ -103,22 +125,20 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.cancelButton.alpha.isZero, self.cancelButton.frame.insetBy(dx: -5.0, dy: -5.0).contains(point) {
return self.cancelButton.view
}
return super.hitTest(point, with: event)
}
@objc func cancelPressed() {
@objc private func cancelPressed() {
self.cancel()
}
func animateIn() {
public func animateIn() {
}
func animateOut() {
public func animateOut() {
}
}

View file

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputAudioRecordingTimeNode",
module_name = "ChatTextInputAudioRecordingTimeNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",
],
)

View file

@ -19,9 +19,9 @@ private final class ChatTextInputAudioRecordingTimeNodeParameters: NSObject {
}
}
private let textFont = Font.regular(15.0)
private let textFont = Font.with(size: 15.0, design: .camera)
final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
public final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private let textNode: TextNode
private var timestamp: Double = 0.0 {
@ -32,9 +32,9 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private let stateDisposable = MetaDisposable()
private var didStart = false
var started = {}
public var started = {}
var audioRecorder: ManagedAudioRecorder? {
public var audioRecorder: ManagedAudioRecorder? {
didSet {
if self.audioRecorder !== oldValue {
if let audioRecorder = self.audioRecorder {
@ -65,7 +65,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private var durationDisposable: MetaDisposable?
var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
public var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
didSet {
if self.videoRecordingStatus !== oldValue {
if self.durationDisposable == nil {
@ -93,7 +93,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private var theme: PresentationTheme
init(theme: PresentationTheme) {
public init(theme: PresentationTheme) {
self.theme = theme
self.textNode = TextNode()
@ -106,13 +106,13 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
self.durationDisposable?.dispose()
}
func updateTheme(theme: PresentationTheme) {
public func updateTheme(theme: PresentationTheme) {
self.theme = theme
self.setNeedsDisplay()
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let makeLayout = TextNode.asyncLayout(self.textNode)
let (size, apply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "0:00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = apply()
@ -120,7 +120,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
return size.size
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp, theme: self.theme)
}

View file

@ -0,0 +1,69 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputPanelNode",
module_name = "ChatTextInputPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/TouchDownGesture",
"//submodules/ImageTransparency",
"//submodules/ActivityIndicator",
"//submodules/AnimationUI",
"//submodules/Speak",
"//submodules/ObjCRuntimeUtils",
"//submodules/AvatarNode",
"//submodules/ContextUI",
"//submodules/InvisibleInkDustNode",
"//submodules/TextInputMenu",
"//submodules/Pasteboard",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ManagedAnimationNode",
"//submodules/AttachmentUI",
"//submodules/TelegramUI/Components/EditableChatTextNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Components/LottieAnimationComponent",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/UndoUI",
"//submodules/PremiumUI",
"//submodules/StickerPeekUI",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/SolidRoundedButtonNode",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
"//submodules/ChatContextQuery",
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TelegramNotices",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/Utils/DeviceModel",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode",
"//submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode",
"//submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator",
"//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel",
"//submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,365 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import GlassBackgroundComponent
import ComponentFlow
import LottieAnimationComponent
import LottieComponent
private let accessoryButtonFont = Font.medium(14.0)
final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundView.ContentView {
private var item: ChatTextInputAccessoryItem
private var theme: PresentationTheme
private var strings: PresentationStrings
private var width: CGFloat
private let iconImageView: UIImageView
private let tintMaskIconImageView: UIImageView
private var textView: ImmediateTextView?
private var tintMaskTextView: ImmediateTextView?
private var animationView: ComponentView<Empty>?
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.iconImageView = UIImageView()
self.tintMaskIconImageView = UIImageView()
let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
super.init(frame: CGRect())
(self.layer as? GlassBackgroundView.ContentLayer)?.targetLayer = self.tintMask.layer
self.isAccessibilityElement = true
self.accessibilityTraits = [.button]
self.iconImageView.isUserInteractionEnabled = false
self.addSubview(self.iconImageView)
self.tintMask.addSubview(self.tintMaskIconImageView)
switch item {
case .input, .botInput, .silentPost:
self.iconImageView.isHidden = true
self.tintMaskIconImageView.isHidden = self.iconImageView.isHidden
self.animationView = ComponentView<Empty>()
self.tintMaskAnimationView = UIImageView()
default:
break
}
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 {
if let textView = self.textView {
self.textView = nil
textView.removeFromSuperview()
}
if let tintMaskTextView = self.tintMaskTextView {
self.tintMaskTextView = nil
tintMaskTextView.removeFromSuperview()
}
}
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
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
strongSelf.layer.allowsGroupOpacity = true
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.layer.allowsGroupOpacity = false
}
}
}
}
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, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
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.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
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) {
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 .commands:
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets())
case let .silentPost(value):
if value {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets())
}
case .suggestPost:
return (PresentationResourcesChat.chatInputTextFieldSuggestPostImage(theme), nil, strings.VoiceOver_SuggestPost, 1.0, UIEdgeInsets())
case let .messageAutoremoveTimeout(timeout):
if let timeout = timeout {
return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).string, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
}
case .scheduledMessages:
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets())
case .gift:
return (PresentationResourcesChat.chatInputTextFieldGiftImage(theme), nil, strings.VoiceOver_GiftPremium, 1.0, UIEdgeInsets())
}
}
private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat {
switch item {
case .input, .botInput, .silentPost, .commands, .scheduledMessages, .gift, .suggestPost:
return 32.0
case let .messageAutoremoveTimeout(timeout):
var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0)
if let _ = timeout, let text = text {
imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0
}
return max(imageWidth, 24.0)
}
}
func updateLayout(item: ChatTextInputAccessoryItem, size: CGSize) {
let previousItem = self.item
self.item = item
let (updatedImage, text, _, _, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: self.theme, strings: self.strings)
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.iconImageView.frame = imageFrame
self.tintMaskIconImageView.frame = imageFrame
if let animationView = self.animationView {
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 animationName: String
var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end)
if case let .silentPost(muted) = item {
if case let .silentPost(previousMuted) = previousItem {
if muted {
animationName = "input_anim_channelMute"
} else {
animationName = "input_anim_channelUnmute"
}
if muted != previousMuted {
animationMode = .animating(loop: false)
}
} else {
animationName = "input_anim_channelMute"
}
} else {
var previousInputMode: ChatTextInputAccessoryItem.InputMode?
var inputMode: ChatTextInputAccessoryItem.InputMode?
switch previousItem {
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
previousInputMode = itemInputMode
default:
break
}
switch item {
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
inputMode = itemInputMode
default:
break
}
if let inputMode = inputMode {
switch inputMode {
case .keyboard:
if let previousInputMode = previousInputMode {
if case .stickers = previousInputMode {
animationName = "input_anim_stickerToKey"
animationMode = .animating(loop: false)
} else if case .emoji = previousInputMode {
animationName = "input_anim_smileToKey"
animationMode = .animating(loop: false)
} else if case .bot = previousInputMode {
animationName = "input_anim_botToKey"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_stickerToKey"
}
} else {
animationName = "input_anim_stickerToKey"
}
case .stickers:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToSticker"
animationMode = .animating(loop: false)
} else if case .emoji = previousInputMode {
animationName = "input_anim_smileToSticker"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToSticker"
}
} else {
animationName = "input_anim_keyToSticker"
}
case .emoji:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToSmile"
animationMode = .animating(loop: false)
} else if case .stickers = previousInputMode {
animationName = "input_anim_stickerToSmile"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToSmile"
}
} else {
animationName = "input_anim_keyToSmile"
}
case .bot:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToBot"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToBot"
}
} else {
animationName = "input_anim_keyToBot"
}
}
} else {
animationName = ""
}
}
let animationSize = animationView.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: self.theme.chat.inputPanel.inputControlColor
)),
environment: {},
containerSize: animationFrame.size
)
if let view = animationView.view as? LottieComponent.View {
view.isUserInteractionEnabled = false
if view.superview == nil {
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
}
if case .animating = animationMode {
view.playOnce()
}
}
}
}
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
}
}
var buttonWidth: CGFloat {
return self.width
}
}

View file

@ -0,0 +1,130 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import SwiftSignalKit
import ChatPresentationInterfaceState
import AnimatedCountLabelNode
import TelegramStringFormatting
private func generateClearImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 17.0, height: 17.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineCap(.round)
context.setLineWidth(1.66)
context.move(to: CGPoint(x: 6.0, y: 6.0))
context.addLine(to: CGPoint(x: 11.0, y: 11.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 6.0, y: 6.0))
context.addLine(to: CGPoint(x: size.width - 11.0, y: 11.0))
context.strokePath()
})
}
final class BoostSlowModeButton: HighlightTrackingButtonNode {
let containerNode: ASDisplayNode
let backgroundNode: ASImageNode
let textNode: ImmediateAnimatedCountLabelNode
let iconNode: ASImageNode
private var updateTimer: SwiftSignalKit.Timer?
var requestUpdate: () -> Void = {}
override init(pointerStyle: PointerStyle? = nil) {
self.containerNode = ASDisplayNode()
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.clipsToBounds = true
self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 2.0), scale: 1.0, colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], locations: [0.0, 1.0], direction: .horizontal)
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateClearImage(color: .white)
self.textNode = ImmediateAnimatedCountLabelNode()
self.textNode.alwaysOneDirection = true
self.textNode.isUserInteractionEnabled = false
super.init(pointerStyle: pointerStyle)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.containerNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false)
} else if let presentationLayer = self.containerNode.layer.presentation() {
self.containerNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
}
}
}
func update(size: CGSize, interfaceState: ChatPresentationInterfaceState) -> CGSize {
var text = ""
if let slowmodeState = interfaceState.slowmodeState {
let relativeTimestamp: CGFloat
switch slowmodeState.variant {
case let .timestamp(validUntilTimestamp):
let timestamp = CGFloat(Date().timeIntervalSince1970)
relativeTimestamp = CGFloat(validUntilTimestamp) - timestamp
case .pendingMessages:
relativeTimestamp = CGFloat(slowmodeState.timeout)
}
self.updateTimer?.invalidate()
if relativeTimestamp >= 0.0 {
text = stringForDuration(Int32(relativeTimestamp))
self.updateTimer = SwiftSignalKit.Timer(timeout: 1.0 / 60.0, repeat: false, completion: { [weak self] in
self?.requestUpdate()
}, queue: .mainQueue())
self.updateTimer?.start()
}
} else {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers])
let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = []
var textCount = 0
for char in text {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: textColor)))
} else {
segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: textColor)))
textCount += 1
}
}
self.textNode.segments = segments
let textSize = self.textNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true)
let totalSize = CGSize(width: textSize.width > 0.0 ? textSize.width + 38.0 : 33.0, height: 33.0)
self.containerNode.bounds = CGRect(origin: .zero, size: totalSize)
self.containerNode.position = CGPoint(x: totalSize.width / 2.0, y: totalSize.height / 2.0)
self.backgroundNode.frame = CGRect(origin: .zero, size: totalSize)
self.backgroundNode.cornerRadius = totalSize.height / 2.0
self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: floorToScreenPixels((totalSize.height - textSize.height) / 2.0)), size: textSize)
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: totalSize.width - icon.size.width - 7.0, y: floorToScreenPixels((totalSize.height - icon.size.height) / 2.0)), size: icon.size)
}
return totalSize
}
}

View file

@ -0,0 +1,78 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ManagedAnimationNode
enum MenuIconNodeState: Equatable {
case menu
case app
case close
}
final class MenuIconNode: ManagedAnimationNode {
private let duration: Double = 0.33
var iconState: MenuIconNodeState = .menu
init() {
super.init(size: CGSize(width: 30.0, height: 30.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
func enqueueState(_ state: MenuIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .close:
switch state {
case .menu:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_closemenu"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .app:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 22), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
}
case .close:
break
}
case .menu:
switch state {
case .close:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 20, endFrame: 20), duration: 0.01))
}
case .app:
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
case .menu:
break
}
case .app:
switch state {
case .close:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 0), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .menu:
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: 0.01))
case .app:
break
}
}
}
}

View file

@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputSlowmodePlaceholderNode",
module_name = "ChatTextInputSlowmodePlaceholderNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramStringFormatting",
"//submodules/AppBundle",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",
],
)

View file

@ -8,7 +8,7 @@ import TelegramStringFormatting
import AppBundle
import ChatPresentationInterfaceState
final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
public final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
private var theme: PresentationTheme
private let iconNode: ASImageNode
private let iconArrowContainerNode: ASDisplayNode
@ -20,7 +20,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
private var timer: SwiftSignalKit.Timer?
init(theme: PresentationTheme) {
public init(theme: PresentationTheme) {
self.theme = theme
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
@ -52,7 +52,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
self.timer?.invalidate()
}
func updateState(_ slowmodeState: ChatSlowmodeState) {
public func updateState(_ slowmodeState: ChatSlowmodeState) {
if self.slowmodeState != slowmodeState {
self.slowmodeState = slowmodeState
self.update()
@ -97,7 +97,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
}
}
func updateLayout(size: CGSize) {
public func updateLayout(size: CGSize) {
self.validLayout = size
var leftInset: CGFloat = 0.0

View file

@ -24,6 +24,8 @@ swift_library(
"//submodules/Components/LottieAnimationComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",

View file

@ -14,6 +14,8 @@ import ComponentFlow
import LottieAnimationComponent
import LottieComponent
import LegacyInstantVideoController
import GlassBackgroundComponent
import ComponentDisplayAdapters
private let offsetThreshold: CGFloat = 10.0
private let dismissOffsetThreshold: CGFloat = 70.0
@ -453,17 +455,23 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM
self.updateAnimation(previousMode: self.mode)
self.pallete = legacyInputMicPalette(from: theme)
self.micDecorationValue?.setColor( self.theme.chat.inputPanel.actionControlFillColor)
self.micDecorationValue?.setColor(self.theme.chat.inputPanel.actionControlFillColor)
(self.micLockValue as? LockView)?.updateTheme(theme)
}
public override func createLockPanelView() -> UIView! {
public override func createLockPanelView() -> (UIView & TGModernConversationInputMicButtonLockPanelView)! {
let isDark: Bool
let tintColor: UIColor
if self.hidesOnLock {
let view = WrapperBlurrredBackgroundView(frame: CGRect(origin: .zero, size: CGSize(width: 40.0, height: 72.0)))
return view
isDark = false
tintColor = UIColor(white: 0.0, alpha: 0.5)
} else {
return super.createLockPanelView()
isDark = self.theme.overallDarkAppearance
tintColor = self.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65)
}
let view = WrapperBlurrredBackgroundView(size: CGSize(width: 40.0, height: 72.0), isDark: isDark, tintColor: tintColor)
return view
}
public func cancelRecording() {
@ -614,16 +622,22 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM
}
}
private class WrapperBlurrredBackgroundView: UIView {
let view: BlurredBackgroundView
private class WrapperBlurrredBackgroundView: UIView, TGModernConversationInputMicButtonLockPanelView {
let isDark: Bool
let glassTintColor: UIColor
override init(frame: CGRect) {
let view = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
view.frame = CGRect(origin: .zero, size: frame.size)
view.update(size: frame.size, cornerRadius: frame.width / 2.0, transition: .immediate)
let view: GlassBackgroundView
init(size: CGSize, isDark: Bool, tintColor: UIColor) {
self.isDark = isDark
self.glassTintColor = tintColor
let view = GlassBackgroundView()
view.frame = CGRect(origin: CGPoint(), size: size)
view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: .immediate)
self.view = view
super.init(frame: frame)
super.init(frame: CGRect(origin: CGPoint(), size: size))
self.addSubview(view)
}
@ -637,7 +651,14 @@ private class WrapperBlurrredBackgroundView: UIView {
return super.frame
} set {
super.frame = newValue
self.view.update(size: newValue.size, cornerRadius: newValue.width / 2.0, transition: .immediate)
self.view.frame = CGRect(origin: CGPoint(), size: newValue.size)
self.view.update(size: newValue.size, cornerRadius: min(newValue.width, newValue.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: .immediate)
}
}
func update(_ size: CGSize) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateFrame(view: self.view, frame: CGRect(origin: CGPoint(), size: size))
self.view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: ComponentTransition(transition))
}
}

View file

@ -387,12 +387,16 @@ public final class GlassBackgroundView: UIView {
if let nativeView = self.nativeView {
let previousFrame = nativeView.frame
transition.containedViewLayoutTransition.animateView {
if transition.animation.isImmediate {
nativeView.layer.cornerRadius = cornerRadius
nativeView.frame = CGRect(origin: CGPoint(), size: size)
} else {
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)
}
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)

View file

@ -1826,7 +1826,8 @@ public class TrimView: UIView {
endPosition: Double,
position: Double,
minDuration: Double,
maxDuration: Double
maxDuration: Double,
isBorderless: Bool
)?
public func update(
@ -1840,10 +1841,13 @@ public class TrimView: UIView {
position: Double,
minDuration: Double,
maxDuration: Double,
isBorderless: Bool = false,
transition: ComponentTransition
) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) {
let isFirstTime = self.params == nil
self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration)
self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration, isBorderless)
self.borderView.isHidden = isBorderless
let effectiveHandleWidth: CGFloat
let fullTrackHeight: CGFloat
@ -1891,7 +1895,7 @@ public class TrimView: UIView {
case .videoMessage:
effectiveHandleWidth = 16.0
fullTrackHeight = 33.0
capsuleOffset = 8.0
capsuleOffset = 10.0
color = theme.chat.inputPanel.panelControlAccentColor
highlightColor = theme.chat.inputPanel.panelControlAccentColor
@ -1918,8 +1922,9 @@ public class TrimView: UIView {
capsuleOffset = 8.0
color = theme.chat.inputPanel.panelControlAccentColor
highlightColor = theme.chat.inputPanel.panelControlAccentColor
self.borderView.isHidden = true
self.zoneView.backgroundColor = UIColor(white: 1.0, alpha: 0.4)
self.zoneView.backgroundColor = .clear
if isFirstTime {
self.borderView.image = generateImage(CGSize(width: 3.0, height: fullTrackHeight), rotatedContext: { size, context in
@ -1941,6 +1946,9 @@ public class TrimView: UIView {
self.leftHandleView.image = handleImage
self.rightHandleView.image = handleImage
self.leftHandleView.image = nil
self.rightHandleView.image = nil
self.leftCapsuleView.backgroundColor = .white
self.rightCapsuleView.backgroundColor = .white
}
@ -1972,7 +1980,7 @@ public class TrimView: UIView {
rightHandleFrame.origin.x = min(rightHandleFrame.origin.x, totalWidth - visualInsets.right - effectiveHandleWidth)
transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame)
let capsuleSize = CGSize(width: 2.0, height: 11.0)
let capsuleSize = CGSize(width: 3.0, height: 12.0)
transition.setFrame(view: self.leftCapsuleView, frame: CGRect(origin: CGPoint(x: capsuleOffset, y: floorToScreenPixels((leftHandleFrame.height - capsuleSize.height) / 2.0)), size: capsuleSize))
transition.setFrame(view: self.rightCapsuleView, frame: CGRect(origin: CGPoint(x: capsuleOffset, y: floorToScreenPixels((leftHandleFrame.height - capsuleSize.height) / 2.0)), size: capsuleSize))

View file

@ -442,6 +442,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, dismissUrlPreview: {
}, dismissForwardMessages: {
}, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View file

@ -829,6 +829,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, dismissUrlPreview: {
}, dismissForwardMessages: {
}, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View file

@ -3932,7 +3932,7 @@ extension ChatControllerImpl {
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true)
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true)
contextController.dismissed = { [weak self] in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
@ -4364,6 +4364,16 @@ extension ChatControllerImpl {
}
return state
})
}, displayUndo: { [weak self] content in
guard let self else {
return
}
self.controllerInteraction?.displayUndo(content)
}, sendEmoji: { [weak self] text, attribute, immediately in
guard let self else {
return
}
self.controllerInteraction?.sendEmoji(text, attribute, immediately)
}, updateHistoryFilter: { [weak self] update in
guard let self else {
return

View file

@ -9961,7 +9961,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
transition.updateSublayerTransformScale(node: self.chatDisplayNode.historyNode, scale: scale)
}
func restrictedSendingContentsText() -> String {
public func restrictedSendingContentsText() -> String {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return self.presentationData.strings.Chat_SendNotAllowedText
}

View file

@ -48,6 +48,7 @@ import SpaceWarpView
import ChatSideTopicsPanel
import GlassBackgroundComponent
import ChatThemeScreen
import ChatTextInputPanelNode
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode
@ -236,6 +237,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
private var leftPanelContainer: ChatControllerTitlePanelNodeContainer
private(set) var leftPanel: (component: AnyComponentWithIdentity<ChatSidePanelEnvironment>, view: ComponentView<ChatSidePanelEnvironment>)?
private var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
private var inputPanelBackgroundBlurMask: UIImageView?
private var inputPanelBackgroundBlurView: VariableBlurView?
@ -855,6 +858,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in
self?.interfaceInteraction?.presentController(controller, nil)
})
self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage
self.textInputPanelNode?.updateHeight = { [weak self] animated in
if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight {
@ -2203,6 +2207,32 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
if "".isEmpty {
var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
if let current = self.bottomBackgroundEdgeEffectNode {
bottomBackgroundEdgeEffectNode = current
} else {
if let value = self.backgroundNode.makeEdgeEffectNode() {
bottomBackgroundEdgeEffectNode = value
self.bottomBackgroundEdgeEffectNode = value
self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view)
}
}
if let bottomBackgroundEdgeEffectNode {
var blurFrame = inputBackgroundFrame
blurFrame.origin.y -= 26.0
blurFrame.size.height += 100.0
transition.updateFrame(node: bottomBackgroundEdgeEffectNode, frame: blurFrame)
bottomBackgroundEdgeEffectNode.update(
rect: blurFrame,
edge: WallpaperEdgeEffectEdge(edge: .bottom, size: 80.0),
containerSize: wallpaperBounds.size,
transition: transition
)
}
}
if !"".isEmpty {
let blurView: VariableBlurView
let blurMask: UIImageView
@ -3931,9 +3961,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
func sendButtonFrame() -> CGRect? {
if let mediaPreviewNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode {
return mediaPreviewNode.convert(mediaPreviewNode.sendButton.frame, to: self)
} else if let frame = self.textInputPanelNode?.actionButtons.frame {
if let frame = self.textInputPanelNode?.actionButtons.frame {
return self.textInputPanelNode?.convert(frame, to: self)
} else {
return nil
@ -3974,10 +4002,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return textInputPanelNode.frameForInputActionButton().flatMap {
return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY)
}
} else if let recordingPreviewPanelNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode {
return recordingPreviewPanelNode.frameForInputActionButton().flatMap {
return $0.offsetBy(dx: recordingPreviewPanelNode.frame.minX, dy: recordingPreviewPanelNode.frame.minY)
}
}
return nil
}
@ -4722,7 +4746,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() {
usedCorrelationId = correlationId
let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: textInput, replyPanel: replyPanel)
let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: ChatMessageTransitionNodeImpl.Source.TextInput(
backgroundView: textInput.backgroundView,
contentView: textInput.contentView,
sourceRect: textInput.sourceRect,
scrollOffset: textInput.scrollOffset
), replyPanel: replyPanel)
self.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: {
})
}

View file

@ -10,6 +10,7 @@ import ChatPresentationInterfaceState
import SwiftSignalKit
import TextFormat
import ChatContextQuery
import ChatTextInputPanelNode
func serviceTasksForChatPresentationIntefaceState(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, updateState: @escaping ((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void) -> [AnyHashable: () -> Disposable] {
var missingEmoji = Set<Int64>()

View file

@ -10,6 +10,7 @@ import ChatInputNode
import ChatEntityKeyboardInputNode
import ChatInputPanelNode
import ChatButtonKeyboardInputNode
import ChatTextInputPanelNode
func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? {
if let inputPanelNode = inputPanelNode, !(inputPanelNode is ChatTextInputPanelNode) {

View file

@ -9,6 +9,7 @@ import ChatBotStartInputPanelNode
import ChatChannelSubscriberInputPanelNode
import ChatMessageSelectionInputPanelNode
import ChatControllerInteraction
import ChatTextInputPanelNode
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 {
@ -418,12 +419,11 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
}
}
var displayBotStartPanel = false
var isScheduledMessages = false
if case .scheduledMessages = chatPresentationInterfaceState.subject {
isScheduledMessages = true
}
var displayBotStartPanel = false
if !isScheduledMessages {
if let _ = chatPresentationInterfaceState.botStartPayload {
@ -436,33 +436,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
}
}
}
let _ = displayBotStartPanel
if displayBotStartPanel, !"".isEmpty {
if let currentPanel = (currentPanel as? ChatBotStartInputPanelNode) ?? (currentSecondaryPanel as? ChatBotStartInputPanelNode) {
currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return (currentPanel, nil)
} else {
let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
panel.context = context
panel.chatControllerInteraction = chatControllerInteraction
panel.interfaceInteraction = interfaceInteraction
return (panel, nil)
}
} else {
if let _ = chatPresentationInterfaceState.interfaceState.mediaDraftState {
if let currentPanel = (currentPanel as? ChatRecordingPreviewInputPanelNode) ?? (currentSecondaryPanel as? ChatRecordingPreviewInputPanelNode) {
return (currentPanel, nil)
} else {
let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme)
panel.context = context
panel.chatControllerInteraction = chatControllerInteraction
panel.interfaceInteraction = interfaceInteraction
return (panel, nil)
}
}
displayInputTextPanel = true
}
displayInputTextPanel = true
}
if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
@ -504,7 +480,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
let panel = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in
interfaceInteraction?.presentController(controller, nil)
})
panel.textInputAccessoryPanel = textInputAccessoryPanel
panel.chatControllerInteraction = chatControllerInteraction
panel.interfaceInteraction = interfaceInteraction
panel.context = context

View file

@ -81,6 +81,25 @@ public enum WallpaperDisplayMode {
}
}
public struct WallpaperEdgeEffectEdge: Equatable {
public enum Edge {
case top
case bottom
}
public var edge: Edge
public var size: CGFloat
public init(edge: Edge, size: CGFloat) {
self.edge = edge
self.size = size
}
}
public protocol WallpaperEdgeEffectNode: ASDisplayNode {
func update(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition)
}
public protocol WallpaperBackgroundNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get }
var rotation: CGFloat { get set }
@ -99,6 +118,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode {
func hasExtraBubbleBackground() -> Bool
func makeDimmedNode() -> ASDisplayNode?
func makeEdgeEffectNode() -> WallpaperEdgeEffectNode?
}
private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOverlayLayer {
@ -493,7 +514,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
if needsGradientBackground, let gradientBackgroundNode = gradientBackgroundSource {
if self.gradientWallpaperNode == nil {
let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true)
gradientWallpaperNode.frame = self.bounds
self.gradientWallpaperNode = gradientWallpaperNode
self.insertSubnode(gradientWallpaperNode, at: 0)
@ -765,6 +786,8 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
private var modelStickerNode: DefaultAnimatedStickerNodeImpl?
fileprivate let edgeEffectNodes = SparseBag<Weak<WallpaperEdgeEffectNodeImpl>>()
private var isSettingUpWallpaper: Bool = false
private struct CachedValidPatternImage {
@ -1188,6 +1211,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.contentNode.alpha = 1.0
self.patternImageLayer.backgroundColor = nil
}
for edgeEffectNode in self.edgeEffectNodes {
if let edgeEffectNode = edgeEffectNode.value {
edgeEffectNode.updatePattern(isInverted: invertPattern)
}
}
default:
self.patternImageDisposable.set(nil)
self.symbolImageDisposable.set(nil)
@ -1198,6 +1227,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.backgroundColor = nil
self.gradientBackgroundNode?.contentView.alpha = 1.0
self.contentNode.alpha = 1.0
for edgeEffectNode in self.edgeEffectNodes {
if let edgeEffectNode = edgeEffectNode.value {
edgeEffectNode.updatePattern(isInverted: false)
}
}
}
}
@ -1670,11 +1705,149 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
public func makeDimmedNode() -> ASDisplayNode? {
if let gradientBackgroundNode = self.gradientBackgroundNode {
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true)
} else {
return nil
}
}
public func makeEdgeEffectNode() -> WallpaperEdgeEffectNode? {
if let gradientBackgroundNode = self.gradientBackgroundNode {
let node = WallpaperEdgeEffectNodeImpl(parentNode: self)
node.cloneNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: false)
return node
} else {
return nil
}
}
}
private final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode {
var cloneNode: GradientBackgroundNode.CloneNode? {
didSet {
if self.cloneNode !== oldValue {
if let cloneNode = self.cloneNode {
self.containerNode.insertSubnode(cloneNode, at: 0)
if let params = self.params {
self.updateImpl(rect: params.rect, edge: params.edge, containerSize: params.containerSize, transition: .immediate)
}
}
}
}
}
private struct Params: Equatable {
let rect: CGRect
let edge: WallpaperEdgeEffectEdge
let containerSize: CGSize
init(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize) {
self.rect = rect
self.edge = edge
self.containerSize = containerSize
}
}
private let containerNode: ASDisplayNode
private let containerMaskingNode: ASDisplayNode
private let overlayNode: ASDisplayNode
private let maskView: UIImageView
private weak var parentNode: WallpaperBackgroundNodeImpl?
private var index: Int?
private var params: Params?
private var isInverted: Bool = false
init(parentNode: WallpaperBackgroundNodeImpl) {
self.parentNode = parentNode
self.containerNode = ASDisplayNode()
self.containerNode.anchorPoint = CGPoint()
self.containerNode.clipsToBounds = true
self.containerMaskingNode = ASDisplayNode()
self.containerMaskingNode.addSubnode(self.containerNode)
self.overlayNode = ASDisplayNode()
self.maskView = UIImageView()
super.init()
self.addSubnode(self.containerMaskingNode)
self.containerMaskingNode.view.mask = self.maskView
self.containerNode.addSubnode(self.overlayNode)
self.index = parentNode.edgeEffectNodes.add(Weak(self))
}
deinit {
if let index = self.index, let parentNode = self.parentNode {
parentNode.edgeEffectNodes.remove(index)
}
}
func updatePattern(isInverted: Bool) {
if self.isInverted != isInverted {
self.isInverted = isInverted
self.overlayNode.backgroundColor = isInverted ? .black : .clear
}
}
func update(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
let params = Params(rect: rect, edge: edge, containerSize: containerSize)
if self.params != params {
self.params = params
self.updateImpl(rect: params.rect, edge: params.edge, containerSize: params.containerSize, transition: transition)
}
}
private func updateImpl(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.containerMaskingNode, frame: CGRect(origin: CGPoint(), size: rect.size))
transition.updateBounds(node: self.containerNode, bounds: CGRect(origin: CGPoint(x: rect.minX, y: rect.minY), size: rect.size))
if self.maskView.image?.size.height != edge.size {
let baseGradientAlpha: CGFloat = 0.75
let numSteps = 8
let firstStep = 1
let firstLocation = 0.0
let colors: [UIColor] = (0 ..< numSteps).map { i 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 = bezierPoint(0.42, 0.0, 0.58, 1.0, step)
return UIColor(white: 1.0, alpha: baseGradientAlpha * value)
}
}
let locations: [CGFloat] = (0 ..< numSteps).map { i in
if i < firstStep {
return 0.0
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
return (firstLocation + (1.0 - firstLocation) * step)
}
}
self.maskView.image = generateGradientImage(
size: CGSize(width: 8.0, height: edge.size),
colors: colors,
locations: locations
)?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(edge.size))
}
transition.updateFrame(view: self.maskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: rect.size))
transition.updateFrame(node: self.overlayNode, frame: CGRect(origin: CGPoint(), size: containerSize))
if let cloneNode = self.cloneNode {
transition.updateFrame(node: cloneNode, frame: CGRect(origin: CGPoint(), size: containerSize))
}
}
}
private protocol WallpaperComponentView: AnyObject {