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 playShakeAnimation()
func removeAd(opaqueId: Data) func removeAd(opaqueId: Data)
func restrictedSendingContentsText() -> String
} }
public protocol ChatMessagePreviewItemNode: AnyObject { public protocol ChatMessagePreviewItemNode: AnyObject {

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import LegacyComponents import LegacyComponents
import GlassBackgroundComponent
public final class VoiceBlobNode: ASDisplayNode { public final class VoiceBlobNode: ASDisplayNode {
public init( public init(
@ -265,6 +266,8 @@ final class BlobNode: ASDisplayNode {
return layer return layer
}() }()
private var backgroundView: GlassBackgroundView?
private var transition: CGFloat = 0 { private var transition: CGFloat = 0 {
didSet { didSet {
guard let currentPoints = currentPoints else { return } guard let currentPoints = currentPoints else { return }
@ -291,6 +294,8 @@ final class BlobNode: ASDisplayNode {
private let hierarchyTrackingNode: HierarchyTrackingNode private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true private var isCurrentlyInHierarchy = true
private var color: UIColor?
init( init(
pointsCount: Int, pointsCount: Int,
minRandomness: CGFloat, minRandomness: CGFloat,
@ -326,6 +331,13 @@ final class BlobNode: ASDisplayNode {
self.layer.addSublayer(self.shapeLayer) self.layer.addSublayer(self.shapeLayer)
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) 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 updateInHierarchy = { [weak self] value in
if let strongSelf = self { if let strongSelf = self {
@ -339,6 +351,8 @@ final class BlobNode: ASDisplayNode {
} }
func setColor(_ color: UIColor, animated: Bool) { func setColor(_ color: UIColor, animated: Bool) {
self.color = color
let previousColor = self.shapeLayer.fillColor let previousColor = self.shapeLayer.fillColor
self.shapeLayer.fillColor = color.cgColor self.shapeLayer.fillColor = color.cgColor
if animated, let previousColor = previousColor, self.isCurrentlyInHierarchy { if animated, let previousColor = previousColor, self.isCurrentlyInHierarchy {
@ -444,6 +458,12 @@ final class BlobNode: ASDisplayNode {
).cgPath ).cgPath
} }
CATransaction.commit() 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/ChatContextQuery",
"//submodules/TooltipUI", "//submodules/TooltipUI",
"//submodules/AudioWaveform", "//submodules/AudioWaveform",
"//submodules/UndoUI",
"//submodules/TextFormat",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

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

View file

@ -201,22 +201,33 @@ public protocol GradientBackgroundPatternOverlayLayer: CALayer {
public final class GradientBackgroundNode: ASDisplayNode { public final class GradientBackgroundNode: ASDisplayNode {
public final class CloneNode: ASImageNode { public final class CloneNode: ASImageNode {
private weak var parentNode: GradientBackgroundNode? private weak var parentNode: GradientBackgroundNode?
private let isDimmed: Bool
private var index: SparseBag<Weak<CloneNode>>.Index? private var index: SparseBag<Weak<CloneNode>>.Index?
public init(parentNode: GradientBackgroundNode) { public init(parentNode: GradientBackgroundNode, isDimmed: Bool) {
self.parentNode = parentNode self.parentNode = parentNode
self.isDimmed = isDimmed
super.init() super.init()
self.displaysAsynchronously = false self.displaysAsynchronously = false
self.index = parentNode.cloneNodes.add(Weak<CloneNode>(self)) if isDimmed {
self.image = parentNode.dimmedImage 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 { deinit {
if let parentNode = self.parentNode, let index = self.index { 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 return nil
} }
} }
private var rawImage: UIImage? {
return self.contentView.image
}
private var validLayout: CGSize? private var validLayout: CGSize?
private let cloneNodes = SparseBag<Weak<CloneNode>>() private let cloneNodes = SparseBag<Weak<CloneNode>>()
private let rawCloneNodes = SparseBag<Weak<CloneNode>>()
private let useSharedAnimationPhase: Bool private let useSharedAnimationPhase: Bool
static var sharedPhase: Int = 0 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 { } else {
let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions) let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions)
self.contentView.image = image self.contentView.image = image
@ -502,6 +536,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
for cloneNode in self.cloneNodes { for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage cloneNode.value?.image = dimmedImage
} }
for cloneNode in self.rawCloneNodes {
cloneNode.value?.image = image
}
completion() completion()
} }
@ -516,6 +553,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
for cloneNode in self.cloneNodes { for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage cloneNode.value?.image = dimmedImage
} }
for cloneNode in self.rawCloneNodes {
cloneNode.value?.image = image
}
self.validPhase = self.phase self.validPhase = self.phase
@ -534,6 +574,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
for cloneNode in self.cloneNodes { for cloneNode in self.cloneNodes {
cloneNode.value?.image = dimmedImage cloneNode.value?.image = dimmedImage
} }
for cloneNode in self.rawCloneNodes {
cloneNode.value?.image = image
}
self.validPhase = self.phase self.validPhase = self.phase

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -176,6 +176,8 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, dismissUrlPreview: { }, dismissUrlPreview: {
}, dismissForwardMessages: { }, dismissForwardMessages: {
}, dismissSuggestPost: { }, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, 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 context: AccountContext
private let presentationContext: ChatPresentationContext? private let presentationContext: ChatPresentationContext?
private let strings: PresentationStrings private let strings: PresentationStrings
let micButtonBackgroundView: GlassBackgroundView public let micButtonBackgroundView: GlassBackgroundView
let micButtonTintMaskView: UIImageView public let micButtonTintMaskView: UIImageView
let micButton: ChatTextInputMediaRecordingButton public let micButton: ChatTextInputMediaRecordingButton
let sendContainerNode: ASDisplayNode public let sendContainerNode: ASDisplayNode
let sendButtonBackgroundView: GlassBackgroundView public let sendButtonBackgroundView: GlassBackgroundView
let sendButton: HighlightTrackingButtonNode public let sendButton: HighlightTrackingButtonNode
var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? public var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
var sendButtonHasApplyIcon = false public var sendButtonHasApplyIcon = false
var animatingSendButton = 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 expandMediaInputButtonBackgroundView: GlassBackgroundView
private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView
private var effectBadgeView: EffectBadgeView? private var effectBadgeView: EffectBadgeView?
var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)? public var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)?
private var gestureRecognizer: ContextGesture? private var gestureRecognizer: ContextGesture?
var sendButtonLongPressEnabled = false { public var sendButtonLongPressEnabled = false {
didSet { didSet {
self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled
} }
@ -165,7 +166,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
private var validLayout: CGSize? 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.context = context
self.presentationContext = presentationContext self.presentationContext = presentationContext
let theme = presentationInterfaceState.theme let theme = presentationInterfaceState.theme
@ -244,7 +245,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
} }
} }
override func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
let gestureRecognizer = ContextGesture(target: nil, action: nil) 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) 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.micButton.updateTheme(theme: theme)
self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor
} }
private var absoluteRect: (CGRect, CGSize)? 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 let previousContaierSize = self.absoluteRect?.1
self.absoluteRect = (rect, containerSize) 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 self.validLayout = size
var innerSize = size var innerSize = size
@ -381,7 +382,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
return innerSize return innerSize
} }
func updateAccessibility() { public func updateAccessibility() {
self.accessibilityTraits = .button self.accessibilityTraits = .button
if !self.micButton.alpha.isZero { if !self.micButton.alpha.isZero {
switch self.micButton.mode { 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 { if self.sendButtonHasApplyIcon || self.effectBadgeView != nil {
let result = UIView() let result = UIView()
result.frame = self.bounds 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 AsyncDisplayKit
import Display import Display
import TelegramPresentationData import TelegramPresentationData
import GlassBackgroundComponent
private let cancelFont = Font.regular(17.0) private let cancelFont = Font.regular(17.0)
final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBackgroundView.ContentView {
private let cancel: () -> Void private let cancel: () -> Void
private let arrowNode: ASImageNode private let arrowView: GlassBackgroundView.ContentImageView
private let labelNode: TextNode private let labelNode: TextNode
private let tintLabelNode: TextNode
private let cancelButton: HighlightableButtonNode private let cancelButton: HighlightableButtonNode
private let strings: PresentationStrings 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.cancel = cancel
self.arrowNode = ASImageNode() self.arrowView = GlassBackgroundView.ContentImageView()
self.arrowNode.isLayerBacked = true self.arrowView.image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate)
self.arrowNode.displayWithoutProcessing = true self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor
self.arrowNode.displaysAsynchronously = false
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)
self.labelNode = TextNode() self.labelNode = TextNode()
self.labelNode.displaysAsynchronously = false self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false self.labelNode.isUserInteractionEnabled = false
self.tintLabelNode = TextNode()
self.tintLabelNode.displaysAsynchronously = false
self.tintLabelNode.isUserInteractionEnabled = false
self.cancelButton = HighlightableButtonNode() self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
self.cancelButton.alpha = 0.0 self.cancelButton.alpha = 0.0
@ -37,21 +45,28 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
self.strings = strings self.strings = strings
super.init() super.init(frame: CGRect())
self.addSubnode(self.arrowNode) self.addSubview(self.arrowView)
self.addSubnode(self.labelNode) self.tintMask.addSubview(self.arrowView.tintMask)
self.addSubview(self.labelNode.view)
self.tintMask.addSubview(self.tintLabelNode.view)
self.addSubnode(self.cancelButton) self.addSubnode(self.cancelButton)
let makeLayout = TextNode.asyncLayout(self.labelNode) 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 _ = 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) 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.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.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)) 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) 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) self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
} }
func updateTheme(theme: PresentationTheme) { required public init?(coder: NSCoder) {
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) fatalError("init(coder:) has not been implemented")
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()
} }
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 { if self.isDisplayingCancel != isDisplayingCancel {
self.isDisplayingCancel = isDisplayingCancel self.isDisplayingCancel = isDisplayingCancel
if isDisplayingCancel { if isDisplayingCancel {
self.arrowNode.alpha = 0.0 self.arrowView.alpha = 0.0
self.labelNode.alpha = 0.0 self.labelNode.alpha = 0.0
self.cancelButton.alpha = 1.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.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.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.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) self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
} }
} else { } else {
self.arrowNode.alpha = 1.0 self.arrowView.alpha = 1.0
self.labelNode.alpha = 1.0 self.labelNode.alpha = 1.0
self.cancelButton.alpha = 0.0 self.cancelButton.alpha = 0.0
if animated { 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.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) 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) { if !self.cancelButton.alpha.isZero, self.cancelButton.frame.insetBy(dx: -5.0, dy: -5.0).contains(point) {
return self.cancelButton.view return self.cancelButton.view
} }
return super.hitTest(point, with: event) return super.hitTest(point, with: event)
} }
@objc func cancelPressed() { @objc private func cancelPressed() {
self.cancel() 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 let textNode: TextNode
private var timestamp: Double = 0.0 { private var timestamp: Double = 0.0 {
@ -32,9 +32,9 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private let stateDisposable = MetaDisposable() private let stateDisposable = MetaDisposable()
private var didStart = false private var didStart = false
var started = {} public var started = {}
var audioRecorder: ManagedAudioRecorder? { public var audioRecorder: ManagedAudioRecorder? {
didSet { didSet {
if self.audioRecorder !== oldValue { if self.audioRecorder !== oldValue {
if let audioRecorder = self.audioRecorder { if let audioRecorder = self.audioRecorder {
@ -65,7 +65,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private var durationDisposable: MetaDisposable? private var durationDisposable: MetaDisposable?
var videoRecordingStatus: InstantVideoControllerRecordingStatus? { public var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
didSet { didSet {
if self.videoRecordingStatus !== oldValue { if self.videoRecordingStatus !== oldValue {
if self.durationDisposable == nil { if self.durationDisposable == nil {
@ -93,7 +93,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
private var theme: PresentationTheme private var theme: PresentationTheme
init(theme: PresentationTheme) { public init(theme: PresentationTheme) {
self.theme = theme self.theme = theme
self.textNode = TextNode() self.textNode = TextNode()
@ -106,13 +106,13 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
self.durationDisposable?.dispose() self.durationDisposable?.dispose()
} }
func updateTheme(theme: PresentationTheme) { public func updateTheme(theme: PresentationTheme) {
self.theme = theme self.theme = theme
self.setNeedsDisplay() self.setNeedsDisplay()
} }
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let makeLayout = TextNode.asyncLayout(self.textNode) 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 (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() let _ = apply()
@ -120,7 +120,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
return size.size 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) 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 AppBundle
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode { public final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
private var theme: PresentationTheme private var theme: PresentationTheme
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let iconArrowContainerNode: ASDisplayNode private let iconArrowContainerNode: ASDisplayNode
@ -20,7 +20,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
private var timer: SwiftSignalKit.Timer? private var timer: SwiftSignalKit.Timer?
init(theme: PresentationTheme) { public init(theme: PresentationTheme) {
self.theme = theme self.theme = theme
self.textNode = ImmediateTextNode() self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false self.textNode.displaysAsynchronously = false
@ -52,7 +52,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
self.timer?.invalidate() self.timer?.invalidate()
} }
func updateState(_ slowmodeState: ChatSlowmodeState) { public func updateState(_ slowmodeState: ChatSlowmodeState) {
if self.slowmodeState != slowmodeState { if self.slowmodeState != slowmodeState {
self.slowmodeState = slowmodeState self.slowmodeState = slowmodeState
self.update() self.update()
@ -97,7 +97,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
} }
} }
func updateLayout(size: CGSize) { public func updateLayout(size: CGSize) {
self.validLayout = size self.validLayout = size
var leftInset: CGFloat = 0.0 var leftInset: CGFloat = 0.0

View file

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

View file

@ -14,6 +14,8 @@ import ComponentFlow
import LottieAnimationComponent import LottieAnimationComponent
import LottieComponent import LottieComponent
import LegacyInstantVideoController import LegacyInstantVideoController
import GlassBackgroundComponent
import ComponentDisplayAdapters
private let offsetThreshold: CGFloat = 10.0 private let offsetThreshold: CGFloat = 10.0
private let dismissOffsetThreshold: CGFloat = 70.0 private let dismissOffsetThreshold: CGFloat = 70.0
@ -453,17 +455,23 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM
self.updateAnimation(previousMode: self.mode) self.updateAnimation(previousMode: self.mode)
self.pallete = legacyInputMicPalette(from: theme) 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) (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 { if self.hidesOnLock {
let view = WrapperBlurrredBackgroundView(frame: CGRect(origin: .zero, size: CGSize(width: 40.0, height: 72.0))) isDark = false
return view tintColor = UIColor(white: 0.0, alpha: 0.5)
} else { } 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() { public func cancelRecording() {
@ -614,16 +622,22 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM
} }
} }
private class WrapperBlurrredBackgroundView: UIView { private class WrapperBlurrredBackgroundView: UIView, TGModernConversationInputMicButtonLockPanelView {
let view: BlurredBackgroundView let isDark: Bool
let glassTintColor: UIColor
override init(frame: CGRect) { let view: GlassBackgroundView
let view = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
view.frame = CGRect(origin: .zero, size: frame.size) init(size: CGSize, isDark: Bool, tintColor: UIColor) {
view.update(size: frame.size, cornerRadius: frame.width / 2.0, transition: .immediate) 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 self.view = view
super.init(frame: frame) super.init(frame: CGRect(origin: CGPoint(), size: size))
self.addSubview(view) self.addSubview(view)
} }
@ -637,7 +651,14 @@ private class WrapperBlurrredBackgroundView: UIView {
return super.frame return super.frame
} set { } set {
super.frame = newValue 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 { if let nativeView = self.nativeView {
let previousFrame = nativeView.frame let previousFrame = nativeView.frame
transition.containedViewLayoutTransition.animateView { if transition.animation.isImmediate {
nativeView.layer.cornerRadius = cornerRadius nativeView.layer.cornerRadius = cornerRadius
nativeView.frame = CGRect(origin: CGPoint(), size: size) 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 { if let backgroundNode = self.backgroundNode {
backgroundNode.updateColor(color: .clear, forceKeepBlur: tintColor.alpha != 1.0, transition: transition.containedViewLayoutTransition) backgroundNode.updateColor(color: .clear, forceKeepBlur: tintColor.alpha != 1.0, transition: transition.containedViewLayoutTransition)

View file

@ -1826,7 +1826,8 @@ public class TrimView: UIView {
endPosition: Double, endPosition: Double,
position: Double, position: Double,
minDuration: Double, minDuration: Double,
maxDuration: Double maxDuration: Double,
isBorderless: Bool
)? )?
public func update( public func update(
@ -1840,10 +1841,13 @@ public class TrimView: UIView {
position: Double, position: Double,
minDuration: Double, minDuration: Double,
maxDuration: Double, maxDuration: Double,
isBorderless: Bool = false,
transition: ComponentTransition transition: ComponentTransition
) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) { ) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) {
let isFirstTime = self.params == nil 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 effectiveHandleWidth: CGFloat
let fullTrackHeight: CGFloat let fullTrackHeight: CGFloat
@ -1891,7 +1895,7 @@ public class TrimView: UIView {
case .videoMessage: case .videoMessage:
effectiveHandleWidth = 16.0 effectiveHandleWidth = 16.0
fullTrackHeight = 33.0 fullTrackHeight = 33.0
capsuleOffset = 8.0 capsuleOffset = 10.0
color = theme.chat.inputPanel.panelControlAccentColor color = theme.chat.inputPanel.panelControlAccentColor
highlightColor = theme.chat.inputPanel.panelControlAccentColor highlightColor = theme.chat.inputPanel.panelControlAccentColor
@ -1918,8 +1922,9 @@ public class TrimView: UIView {
capsuleOffset = 8.0 capsuleOffset = 8.0
color = theme.chat.inputPanel.panelControlAccentColor color = theme.chat.inputPanel.panelControlAccentColor
highlightColor = 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 { if isFirstTime {
self.borderView.image = generateImage(CGSize(width: 3.0, height: fullTrackHeight), rotatedContext: { size, context in 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.leftHandleView.image = handleImage
self.rightHandleView.image = handleImage self.rightHandleView.image = handleImage
self.leftHandleView.image = nil
self.rightHandleView.image = nil
self.leftCapsuleView.backgroundColor = .white self.leftCapsuleView.backgroundColor = .white
self.rightCapsuleView.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) rightHandleFrame.origin.x = min(rightHandleFrame.origin.x, totalWidth - visualInsets.right - effectiveHandleWidth)
transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame) 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.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)) 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: { }, dismissUrlPreview: {
}, dismissForwardMessages: { }, dismissForwardMessages: {
}, dismissSuggestPost: { }, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, toggleChatSidebarMode: {

View file

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

View file

@ -3932,7 +3932,7 @@ extension ChatControllerImpl {
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(interactive: true, { strongSelf.updateChatPresentationInterfaceState(interactive: true, {
@ -4364,6 +4364,16 @@ extension ChatControllerImpl {
} }
return state 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 }, updateHistoryFilter: { [weak self] update in
guard let self else { guard let self else {
return return

View file

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

View file

@ -48,6 +48,7 @@ import SpaceWarpView
import ChatSideTopicsPanel import ChatSideTopicsPanel
import GlassBackgroundComponent import GlassBackgroundComponent
import ChatThemeScreen import ChatThemeScreen
import ChatTextInputPanelNode
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode let itemNode: OverlayMediaItemNode
@ -236,6 +237,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
private var leftPanelContainer: ChatControllerTitlePanelNodeContainer private var leftPanelContainer: ChatControllerTitlePanelNodeContainer
private(set) var leftPanel: (component: AnyComponentWithIdentity<ChatSidePanelEnvironment>, view: ComponentView<ChatSidePanelEnvironment>)? private(set) var leftPanel: (component: AnyComponentWithIdentity<ChatSidePanelEnvironment>, view: ComponentView<ChatSidePanelEnvironment>)?
private var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
private var inputPanelBackgroundBlurMask: UIImageView? private var inputPanelBackgroundBlurMask: UIImageView?
private var inputPanelBackgroundBlurView: VariableBlurView? 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.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in
self?.interfaceInteraction?.presentController(controller, nil) self?.interfaceInteraction?.presentController(controller, nil)
}) })
self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage
self.textInputPanelNode?.updateHeight = { [weak self] animated in self.textInputPanelNode?.updateHeight = { [weak self] animated in
if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { 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 { if !"".isEmpty {
let blurView: VariableBlurView let blurView: VariableBlurView
let blurMask: UIImageView let blurMask: UIImageView
@ -3931,9 +3961,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
} }
func sendButtonFrame() -> CGRect? { func sendButtonFrame() -> CGRect? {
if let mediaPreviewNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode { if let frame = self.textInputPanelNode?.actionButtons.frame {
return mediaPreviewNode.convert(mediaPreviewNode.sendButton.frame, to: self)
} else if let frame = self.textInputPanelNode?.actionButtons.frame {
return self.textInputPanelNode?.convert(frame, to: self) return self.textInputPanelNode?.convert(frame, to: self)
} else { } else {
return nil return nil
@ -3974,10 +4002,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return textInputPanelNode.frameForInputActionButton().flatMap { return textInputPanelNode.frameForInputActionButton().flatMap {
return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) 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 return nil
} }
@ -4722,7 +4746,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
} }
if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() { if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() {
usedCorrelationId = correlationId 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: { self.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: {
}) })
} }

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import ChatBotStartInputPanelNode
import ChatChannelSubscriberInputPanelNode import ChatChannelSubscriberInputPanelNode
import ChatMessageSelectionInputPanelNode import ChatMessageSelectionInputPanelNode
import ChatControllerInteraction import ChatControllerInteraction
import ChatTextInputPanelNode
func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) {
if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { 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 var isScheduledMessages = false
if case .scheduledMessages = chatPresentationInterfaceState.subject { if case .scheduledMessages = chatPresentationInterfaceState.subject {
isScheduledMessages = true isScheduledMessages = true
} }
var displayBotStartPanel = false
if !isScheduledMessages { if !isScheduledMessages {
if let _ = chatPresentationInterfaceState.botStartPayload { if let _ = chatPresentationInterfaceState.botStartPayload {
@ -436,33 +436,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
} }
} }
} }
let _ = displayBotStartPanel
if displayBotStartPanel, !"".isEmpty { displayInputTextPanel = true
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
}
} }
if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { 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 let panel = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in
interfaceInteraction?.presentController(controller, nil) interfaceInteraction?.presentController(controller, nil)
}) })
panel.textInputAccessoryPanel = textInputAccessoryPanel
panel.chatControllerInteraction = chatControllerInteraction panel.chatControllerInteraction = chatControllerInteraction
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
panel.context = context 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 { public protocol WallpaperBackgroundNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get } var isReady: Signal<Bool, NoError> { get }
var rotation: CGFloat { get set } var rotation: CGFloat { get set }
@ -99,6 +118,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode {
func hasExtraBubbleBackground() -> Bool func hasExtraBubbleBackground() -> Bool
func makeDimmedNode() -> ASDisplayNode? func makeDimmedNode() -> ASDisplayNode?
func makeEdgeEffectNode() -> WallpaperEdgeEffectNode?
} }
private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOverlayLayer { private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOverlayLayer {
@ -493,7 +514,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
if needsGradientBackground, let gradientBackgroundNode = gradientBackgroundSource { if needsGradientBackground, let gradientBackgroundNode = gradientBackgroundSource {
if self.gradientWallpaperNode == nil { if self.gradientWallpaperNode == nil {
let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true)
gradientWallpaperNode.frame = self.bounds gradientWallpaperNode.frame = self.bounds
self.gradientWallpaperNode = gradientWallpaperNode self.gradientWallpaperNode = gradientWallpaperNode
self.insertSubnode(gradientWallpaperNode, at: 0) self.insertSubnode(gradientWallpaperNode, at: 0)
@ -765,6 +786,8 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
private var modelStickerNode: DefaultAnimatedStickerNodeImpl? private var modelStickerNode: DefaultAnimatedStickerNodeImpl?
fileprivate let edgeEffectNodes = SparseBag<Weak<WallpaperEdgeEffectNodeImpl>>()
private var isSettingUpWallpaper: Bool = false private var isSettingUpWallpaper: Bool = false
private struct CachedValidPatternImage { private struct CachedValidPatternImage {
@ -1188,6 +1211,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.contentNode.alpha = 1.0 self.contentNode.alpha = 1.0
self.patternImageLayer.backgroundColor = nil self.patternImageLayer.backgroundColor = nil
} }
for edgeEffectNode in self.edgeEffectNodes {
if let edgeEffectNode = edgeEffectNode.value {
edgeEffectNode.updatePattern(isInverted: invertPattern)
}
}
default: default:
self.patternImageDisposable.set(nil) self.patternImageDisposable.set(nil)
self.symbolImageDisposable.set(nil) self.symbolImageDisposable.set(nil)
@ -1198,6 +1227,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.backgroundColor = nil self.backgroundColor = nil
self.gradientBackgroundNode?.contentView.alpha = 1.0 self.gradientBackgroundNode?.contentView.alpha = 1.0
self.contentNode.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? { public func makeDimmedNode() -> ASDisplayNode? {
if let gradientBackgroundNode = self.gradientBackgroundNode { if let gradientBackgroundNode = self.gradientBackgroundNode {
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true)
} else { } else {
return nil 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 { private protocol WallpaperComponentView: AnyObject {