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:
commit
5de7e74180
45 changed files with 2469 additions and 2192 deletions
|
|
@ -1096,6 +1096,8 @@ public protocol ChatController: ViewController {
|
|||
func playShakeAnimation()
|
||||
|
||||
func removeAd(opaqueId: Data)
|
||||
|
||||
func restrictedSendingContentsText() -> String
|
||||
}
|
||||
|
||||
public protocol ChatMessagePreviewItemNode: AnyObject {
|
||||
|
|
|
|||
|
|
@ -1374,7 +1374,7 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
let inputNode: ASDisplayNode
|
||||
let accessoryPanelNode: ASDisplayNode?
|
||||
let menuButtonNode: ASDisplayNode
|
||||
let menuButtonBackgroundNode: ASDisplayNode
|
||||
let menuButtonBackgroundView: UIView
|
||||
let menuIconNode: ASDisplayNode
|
||||
let menuTextNode: ASDisplayNode
|
||||
let prepareForDismiss: () -> Void
|
||||
|
|
@ -1383,7 +1383,7 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
inputNode: ASDisplayNode,
|
||||
accessoryPanelNode: ASDisplayNode?,
|
||||
menuButtonNode: ASDisplayNode,
|
||||
menuButtonBackgroundNode: ASDisplayNode,
|
||||
menuButtonBackgroundView: UIView,
|
||||
menuIconNode: ASDisplayNode,
|
||||
menuTextNode: ASDisplayNode,
|
||||
prepareForDismiss: @escaping () -> Void
|
||||
|
|
@ -1391,7 +1391,7 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
self.inputNode = inputNode
|
||||
self.accessoryPanelNode = accessoryPanelNode
|
||||
self.menuButtonNode = menuButtonNode
|
||||
self.menuButtonBackgroundNode = menuButtonBackgroundNode
|
||||
self.menuButtonBackgroundView = menuButtonBackgroundView
|
||||
self.menuIconNode = menuIconNode
|
||||
self.menuTextNode = menuTextNode
|
||||
self.prepareForDismiss = prepareForDismiss
|
||||
|
|
|
|||
|
|
@ -1276,6 +1276,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
}, dismissUrlPreview: {
|
||||
}, dismissForwardMessages: {
|
||||
}, dismissSuggestPost: {
|
||||
}, displayUndo: { _ in
|
||||
}, sendEmoji: { _, _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
|
@ -1628,7 +1630,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.animatingTransition = true
|
||||
|
||||
let targetButtonColor = self.mainButtonNode.backgroundColor
|
||||
self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundNode.backgroundColor
|
||||
self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundView.backgroundColor
|
||||
transition.updateBackgroundColor(node: self.mainButtonNode, color: targetButtonColor ?? .clear)
|
||||
|
||||
transition.animateFrame(layer: self.mainButtonNode.layer, from: inputTransition.menuButtonNode.frame)
|
||||
|
|
@ -1686,7 +1688,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
let sourceButtonColor = self.mainButtonNode.backgroundColor
|
||||
transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundNode.backgroundColor ?? .clear)
|
||||
transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundView.backgroundColor ?? .clear)
|
||||
|
||||
let sourceButtonFrame = self.mainButtonNode.frame
|
||||
transition.updateFrame(node: self.mainButtonNode, frame: inputTransition.menuButtonNode.frame)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ swift_library(
|
|||
"//submodules/LegacyComponents",
|
||||
"//submodules/MetalEngine",
|
||||
"//submodules/TelegramUI/Components/Calls/CallScreen",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
import Display
|
||||
import LegacyComponents
|
||||
import GlassBackgroundComponent
|
||||
|
||||
public final class VoiceBlobNode: ASDisplayNode {
|
||||
public init(
|
||||
|
|
@ -265,6 +266,8 @@ final class BlobNode: ASDisplayNode {
|
|||
return layer
|
||||
}()
|
||||
|
||||
private var backgroundView: GlassBackgroundView?
|
||||
|
||||
private var transition: CGFloat = 0 {
|
||||
didSet {
|
||||
guard let currentPoints = currentPoints else { return }
|
||||
|
|
@ -291,6 +294,8 @@ final class BlobNode: ASDisplayNode {
|
|||
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||
private var isCurrentlyInHierarchy = true
|
||||
|
||||
private var color: UIColor?
|
||||
|
||||
init(
|
||||
pointsCount: Int,
|
||||
minRandomness: CGFloat,
|
||||
|
|
@ -326,6 +331,13 @@ final class BlobNode: ASDisplayNode {
|
|||
self.layer.addSublayer(self.shapeLayer)
|
||||
|
||||
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||
|
||||
if isCircle {
|
||||
let backgroundView = GlassBackgroundView()
|
||||
self.backgroundView = backgroundView
|
||||
self.shapeLayer.removeFromSuperlayer()
|
||||
self.view.addSubview(backgroundView)
|
||||
}
|
||||
|
||||
updateInHierarchy = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
|
|
@ -339,6 +351,8 @@ final class BlobNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
func setColor(_ color: UIColor, animated: Bool) {
|
||||
self.color = color
|
||||
|
||||
let previousColor = self.shapeLayer.fillColor
|
||||
self.shapeLayer.fillColor = color.cgColor
|
||||
if animated, let previousColor = previousColor, self.isCurrentlyInHierarchy {
|
||||
|
|
@ -444,6 +458,12 @@ final class BlobNode: ASDisplayNode {
|
|||
).cgPath
|
||||
}
|
||||
CATransaction.commit()
|
||||
|
||||
if let backgroundView = self.backgroundView, let color = self.color {
|
||||
let halfWidth = floor(self.bounds.width * self.minScale)
|
||||
backgroundView.update(size: CGSize(width: halfWidth, height: halfWidth), cornerRadius: halfWidth * 0.5, isDark: false, tintColor: color, transition: .immediate)
|
||||
backgroundView.frame = CGRect(origin: CGPoint(x: (self.bounds.width - halfWidth) * 0.5, y: (self.bounds.height - halfWidth) * 0.5), size: CGSize(width: halfWidth, height: halfWidth))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ swift_library(
|
|||
"//submodules/ChatContextQuery",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/AudioWaveform",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import Display
|
|||
import AccountContext
|
||||
import ContextUI
|
||||
import TooltipUI
|
||||
import UndoUI
|
||||
import TextFormat
|
||||
|
||||
public enum ChatLoadingMessageSubject {
|
||||
case generic
|
||||
|
|
@ -190,6 +192,8 @@ public final class ChatPanelInterfaceInteraction {
|
|||
public let dismissUrlPreview: () -> Void
|
||||
public let dismissForwardMessages: () -> Void
|
||||
public let dismissSuggestPost: () -> Void
|
||||
public let displayUndo: (UndoOverlayContent) -> Void
|
||||
public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void
|
||||
public let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
public let chatController: () -> ViewController?
|
||||
public let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
|
|
@ -312,6 +316,8 @@ public final class ChatPanelInterfaceInteraction {
|
|||
dismissUrlPreview: @escaping () -> Void,
|
||||
dismissForwardMessages: @escaping () -> Void,
|
||||
dismissSuggestPost: @escaping () -> Void,
|
||||
displayUndo: @escaping (UndoOverlayContent) -> Void,
|
||||
sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void,
|
||||
updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void,
|
||||
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
|
||||
toggleChatSidebarMode: @escaping () -> Void,
|
||||
|
|
@ -437,6 +443,8 @@ public final class ChatPanelInterfaceInteraction {
|
|||
self.dismissUrlPreview = dismissUrlPreview
|
||||
self.dismissForwardMessages = dismissForwardMessages
|
||||
self.dismissSuggestPost = dismissSuggestPost
|
||||
self.displayUndo = displayUndo
|
||||
self.sendEmoji = sendEmoji
|
||||
self.updateHistoryFilter = updateHistoryFilter
|
||||
self.updateChatLocationThread = updateChatLocationThread
|
||||
self.toggleChatSidebarMode = toggleChatSidebarMode
|
||||
|
|
@ -571,6 +579,8 @@ public final class ChatPanelInterfaceInteraction {
|
|||
}, dismissUrlPreview: {
|
||||
}, dismissForwardMessages: {
|
||||
}, dismissSuggestPost: {
|
||||
}, displayUndo: { _ in
|
||||
}, sendEmoji: { _, _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
|
|
|||
|
|
@ -201,22 +201,33 @@ public protocol GradientBackgroundPatternOverlayLayer: CALayer {
|
|||
public final class GradientBackgroundNode: ASDisplayNode {
|
||||
public final class CloneNode: ASImageNode {
|
||||
private weak var parentNode: GradientBackgroundNode?
|
||||
private let isDimmed: Bool
|
||||
private var index: SparseBag<Weak<CloneNode>>.Index?
|
||||
|
||||
public init(parentNode: GradientBackgroundNode) {
|
||||
public init(parentNode: GradientBackgroundNode, isDimmed: Bool) {
|
||||
self.parentNode = parentNode
|
||||
self.isDimmed = isDimmed
|
||||
|
||||
super.init()
|
||||
|
||||
self.displaysAsynchronously = false
|
||||
|
||||
self.index = parentNode.cloneNodes.add(Weak<CloneNode>(self))
|
||||
self.image = parentNode.dimmedImage
|
||||
if isDimmed {
|
||||
self.index = parentNode.cloneNodes.add(Weak<CloneNode>(self))
|
||||
self.image = parentNode.dimmedImage
|
||||
} else {
|
||||
self.index = parentNode.rawCloneNodes.add(Weak<CloneNode>(self))
|
||||
self.image = parentNode.rawImage
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let parentNode = self.parentNode, let index = self.index {
|
||||
parentNode.cloneNodes.remove(index)
|
||||
if self.isDimmed {
|
||||
parentNode.cloneNodes.remove(index)
|
||||
} else {
|
||||
parentNode.rawCloneNodes.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -258,9 +269,14 @@ public final class GradientBackgroundNode: ASDisplayNode {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var rawImage: UIImage? {
|
||||
return self.contentView.image
|
||||
}
|
||||
|
||||
private var validLayout: CGSize?
|
||||
private let cloneNodes = SparseBag<Weak<CloneNode>>()
|
||||
private let rawCloneNodes = SparseBag<Weak<CloneNode>>()
|
||||
|
||||
private let useSharedAnimationPhase: Bool
|
||||
static var sharedPhase: Int = 0
|
||||
|
|
@ -490,6 +506,24 @@ public final class GradientBackgroundNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.rawCloneNodes.isEmpty {
|
||||
let cloneAnimation = CAKeyframeAnimation(keyPath: "contents")
|
||||
cloneAnimation.values = images.map { $0.0.cgImage! }
|
||||
cloneAnimation.duration = animation.duration
|
||||
cloneAnimation.calculationMode = animation.calculationMode
|
||||
cloneAnimation.isRemovedOnCompletion = animation.isRemovedOnCompletion
|
||||
cloneAnimation.fillMode = animation.fillMode
|
||||
cloneAnimation.beginTime = animation.beginTime
|
||||
|
||||
for cloneNode in self.rawCloneNodes {
|
||||
if let value = cloneNode.value {
|
||||
value.image = images.last?.0
|
||||
value.layer.removeAnimation(forKey: "contents")
|
||||
value.layer.add(cloneAnimation, forKey: "contents")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions)
|
||||
self.contentView.image = image
|
||||
|
|
@ -502,6 +536,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
|
|||
for cloneNode in self.cloneNodes {
|
||||
cloneNode.value?.image = dimmedImage
|
||||
}
|
||||
for cloneNode in self.rawCloneNodes {
|
||||
cloneNode.value?.image = image
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
|
|
@ -516,6 +553,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
|
|||
for cloneNode in self.cloneNodes {
|
||||
cloneNode.value?.image = dimmedImage
|
||||
}
|
||||
for cloneNode in self.rawCloneNodes {
|
||||
cloneNode.value?.image = image
|
||||
}
|
||||
|
||||
self.validPhase = self.phase
|
||||
|
||||
|
|
@ -534,6 +574,9 @@ public final class GradientBackgroundNode: ASDisplayNode {
|
|||
for cloneNode in self.cloneNodes {
|
||||
cloneNode.value?.image = dimmedImage
|
||||
}
|
||||
for cloneNode in self.rawCloneNodes {
|
||||
cloneNode.value?.image = image
|
||||
}
|
||||
|
||||
self.validPhase = self.phase
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@
|
|||
|
||||
@end
|
||||
|
||||
@protocol TGModernConversationInputMicButtonLockPanelView <NSObject>
|
||||
|
||||
- (void)updateSize:(CGSize)size;
|
||||
|
||||
@end
|
||||
|
||||
@interface TGModernConversationInputMicButton : UIButton
|
||||
|
||||
@property (nonatomic, weak) id<TGModernConversationInputMicButtonDelegate> delegate;
|
||||
|
|
@ -88,6 +94,6 @@
|
|||
- (void)_commitLocked;
|
||||
|
||||
- (void)setHidesPanelOnLock;
|
||||
- (UIView *)createLockPanelView;
|
||||
- (UIView<TGModernConversationInputMicButtonLockPanelView> *)createLockPanelView;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -98,6 +98,17 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
|
|||
|
||||
@end
|
||||
|
||||
@interface TGModernConversationInputMicButtonLockPanelViewNativeImpl : UIImageView<TGModernConversationInputMicButtonLockPanelView>
|
||||
|
||||
@end
|
||||
|
||||
@implementation TGModernConversationInputMicButtonLockPanelViewNativeImpl
|
||||
|
||||
- (void)updateSize:(CGSize)size {
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface TGModernConversationInputMicButton () <UIGestureRecognizerDelegate>
|
||||
{
|
||||
CGPoint _touchLocation;
|
||||
|
|
@ -116,7 +127,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
|
|||
UIImageView *_innerIconView;
|
||||
|
||||
UIView *_lockPanelWrapperView;
|
||||
UIView *_lockPanelView;
|
||||
UIView<TGModernConversationInputMicButtonLockPanelView> *_lockPanelView;
|
||||
UIImageView *_lockArrowView;
|
||||
TGModernConversationInputLockView *_lockView;
|
||||
UIImage *_previousIcon;
|
||||
|
|
@ -353,8 +364,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
|
|||
return stopButtonImage;
|
||||
}
|
||||
|
||||
- (UIView *)createLockPanelView {
|
||||
UIImageView *view = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)];
|
||||
- (UIView<TGModernConversationInputMicButtonLockPanelView> *)createLockPanelView {
|
||||
TGModernConversationInputMicButtonLockPanelViewNativeImpl *view = [[TGModernConversationInputMicButtonLockPanelViewNativeImpl alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)];
|
||||
view.userInteractionEnabled = true;
|
||||
view.image = [self panelBackgroundImage];
|
||||
return view;
|
||||
|
|
@ -634,6 +645,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
|
|||
[snapshotView removeFromSuperview];
|
||||
}];
|
||||
|
||||
[_lockPanelView updateSize:CGSizeMake(_lockPanelView.frame.size.width, 72.0f - 32.0f)];
|
||||
|
||||
[UIView animateWithDuration:0.2 animations:^
|
||||
{
|
||||
snapshotView.alpha = 0.0f;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ final class CustomPasscodeBackground: PasscodeBackground {
|
|||
|
||||
func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? {
|
||||
if self.inverted, let backgroundNode = backgroundNode as? GradientBackgroundNode {
|
||||
return GradientBackgroundNode.CloneNode(parentNode: backgroundNode)
|
||||
return GradientBackgroundNode.CloneNode(parentNode: backgroundNode, isDimmed: true)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -486,10 +486,12 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/FaceScanScreen",
|
||||
"//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist",
|
||||
"//submodules/TelegramUI/Components/ChatThemeScreen",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode",
|
||||
"//submodules/ContactsHelper",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
|
|||
private let backgroundColorNode: ASDisplayNode
|
||||
private let backgroundAdditionalColorNode: ASDisplayNode
|
||||
|
||||
private let shadowNode: ASImageNode
|
||||
private let highlightNode: ASImageNode
|
||||
|
||||
private let textNode: ImmediateTextNode
|
||||
|
|
@ -37,24 +36,17 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
|
|||
self.backgroundContainerNode.clipsToBounds = true
|
||||
self.backgroundContainerNode.allowsGroupOpacity = true
|
||||
self.backgroundContainerNode.isUserInteractionEnabled = false
|
||||
self.backgroundContainerNode.cornerRadius = 5.0
|
||||
if #available(iOS 13.0, *) {
|
||||
self.backgroundContainerNode.layer.cornerCurve = .continuous
|
||||
}
|
||||
self.backgroundContainerNode.cornerRadius = 10.0
|
||||
self.backgroundContainerNode.layer.cornerCurve = .continuous
|
||||
|
||||
self.backgroundColorNode = ASDisplayNode()
|
||||
self.backgroundColorNode.cornerRadius = 5.0
|
||||
if #available(iOS 13.0, *) {
|
||||
self.backgroundColorNode.layer.cornerCurve = .continuous
|
||||
}
|
||||
self.backgroundColorNode.cornerRadius = 10.0
|
||||
self.backgroundColorNode.layer.cornerCurve = .continuous
|
||||
|
||||
self.backgroundAdditionalColorNode = ASDisplayNode()
|
||||
self.backgroundAdditionalColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
|
||||
self.backgroundAdditionalColorNode.isHidden = true
|
||||
|
||||
self.shadowNode = ASImageNode()
|
||||
self.shadowNode.isUserInteractionEnabled = false
|
||||
|
||||
self.highlightNode = ASImageNode()
|
||||
self.highlightNode.isUserInteractionEnabled = false
|
||||
|
||||
|
|
@ -73,7 +65,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
|
|||
self.backgroundContainerNode.addSubnode(self.backgroundAdditionalColorNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.backgroundContainerNode.addSubnode(self.shadowNode)
|
||||
self.backgroundContainerNode.addSubnode(self.highlightNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
|
|
@ -146,7 +137,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
|
|||
self.theme = theme
|
||||
|
||||
self.highlightNode.image = PresentationResourcesChat.chatInputButtonPanelButtonHighlightImage(theme)
|
||||
self.shadowNode.image = PresentationResourcesChat.chatInputButtonPanelButtonShadowImage(theme)
|
||||
|
||||
self.updateIcon()
|
||||
}
|
||||
|
|
@ -182,7 +172,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo
|
|||
self.backgroundNode?.frame = self.backgroundColorNode.frame
|
||||
|
||||
self.highlightNode.frame = self.bounds
|
||||
self.shadowNode.frame = self.bounds
|
||||
|
||||
if let (rect, containerSize) = self.absoluteRect {
|
||||
self.update(rect: rect, within: containerSize, transition: .immediate)
|
||||
|
|
@ -201,7 +190,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
|
|||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
||||
|
|
@ -220,9 +208,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
|
|||
|
||||
self.backgroundColorNode = ASDisplayNode()
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundColorNode)
|
||||
|
|
@ -232,8 +217,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
|
|||
self.scrollNode.view.canCancelContentTouches = true
|
||||
self.scrollNode.view.alwaysBounceHorizontal = false
|
||||
self.scrollNode.view.alwaysBounceVertical = false
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
|
|
@ -264,8 +247,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
|
|||
}
|
||||
|
||||
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
|
||||
if self.backgroundNode == nil {
|
||||
if let backgroundNode = self.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||
self.backgroundNode = backgroundNode
|
||||
|
|
@ -277,7 +258,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
|
|||
if updatedTheme {
|
||||
self.theme = interfaceState.theme
|
||||
|
||||
self.separatorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelSeparatorColor
|
||||
self.backgroundColorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelBackgroundColor
|
||||
}
|
||||
|
||||
|
|
@ -296,8 +276,8 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
|
|||
self.message = interfaceState.keyboardButtonsMessage
|
||||
|
||||
if let markup = validatedMarkup {
|
||||
let verticalInset: CGFloat = 10.0
|
||||
let sideInset: CGFloat = 6.0 + leftInset
|
||||
let verticalInset: CGFloat = 16.0
|
||||
let sideInset: CGFloat = 16.0 + leftInset
|
||||
var buttonHeight: CGFloat = 43.0
|
||||
let columnSpacing: CGFloat = 6.0
|
||||
let rowSpacing: CGFloat = 5.0
|
||||
|
|
|
|||
|
|
@ -176,6 +176,8 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
|||
}, dismissUrlPreview: {
|
||||
}, dismissForwardMessages: {
|
||||
}, dismissSuggestPost: {
|
||||
}, displayUndo: { _ in
|
||||
}, sendEmoji: { _, _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -128,33 +128,34 @@ private final class EffectBadgeView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode {
|
||||
public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode {
|
||||
private let context: AccountContext
|
||||
private let presentationContext: ChatPresentationContext?
|
||||
private let strings: PresentationStrings
|
||||
|
||||
let micButtonBackgroundView: GlassBackgroundView
|
||||
let micButtonTintMaskView: UIImageView
|
||||
let micButton: ChatTextInputMediaRecordingButton
|
||||
public let micButtonBackgroundView: GlassBackgroundView
|
||||
public let micButtonTintMaskView: UIImageView
|
||||
public let micButton: ChatTextInputMediaRecordingButton
|
||||
|
||||
let sendContainerNode: ASDisplayNode
|
||||
let sendButtonBackgroundView: GlassBackgroundView
|
||||
let sendButton: HighlightTrackingButtonNode
|
||||
var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
|
||||
var sendButtonHasApplyIcon = false
|
||||
var animatingSendButton = false
|
||||
public let sendContainerNode: ASDisplayNode
|
||||
public let sendButtonBackgroundView: GlassBackgroundView
|
||||
public let sendButton: HighlightTrackingButtonNode
|
||||
public var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
|
||||
public var sendButtonHasApplyIcon = false
|
||||
public var animatingSendButton = false
|
||||
|
||||
let textNode: ImmediateAnimatedCountLabelNode
|
||||
public let textNode: ImmediateAnimatedCountLabelNode
|
||||
|
||||
let expandMediaInputButton: HighlightTrackingButton
|
||||
public let expandMediaInputButton: HighlightTrackingButton
|
||||
private let expandMediaInputButtonBackgroundView: GlassBackgroundView
|
||||
private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView
|
||||
|
||||
private var effectBadgeView: EffectBadgeView?
|
||||
|
||||
var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)?
|
||||
public var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)?
|
||||
|
||||
private var gestureRecognizer: ContextGesture?
|
||||
var sendButtonLongPressEnabled = false {
|
||||
public var sendButtonLongPressEnabled = false {
|
||||
didSet {
|
||||
self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled
|
||||
}
|
||||
|
|
@ -165,7 +166,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
|
|||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
self.presentationContext = presentationContext
|
||||
let theme = presentationInterfaceState.theme
|
||||
|
|
@ -244,7 +245,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
|
|||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let gestureRecognizer = ContextGesture(target: nil, action: nil)
|
||||
|
|
@ -261,13 +262,13 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
|
|||
self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.sendButtonBackgroundView, style: .lift)
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
||||
public func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
||||
self.micButton.updateTheme(theme: theme)
|
||||
self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor
|
||||
}
|
||||
|
||||
private var absoluteRect: (CGRect, CGSize)?
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let previousContaierSize = self.absoluteRect?.1
|
||||
self.absoluteRect = (rect, containerSize)
|
||||
|
||||
|
|
@ -278,7 +279,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
|
|||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize {
|
||||
public func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize {
|
||||
self.validLayout = size
|
||||
|
||||
var innerSize = size
|
||||
|
|
@ -381,7 +382,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
|
|||
return innerSize
|
||||
}
|
||||
|
||||
func updateAccessibility() {
|
||||
public func updateAccessibility() {
|
||||
self.accessibilityTraits = .button
|
||||
if !self.micButton.alpha.isZero {
|
||||
switch self.micButton.mode {
|
||||
|
|
@ -398,7 +399,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
|
|||
}
|
||||
}
|
||||
|
||||
func makeCustomContents() -> UIView? {
|
||||
public func makeCustomContents() -> UIView? {
|
||||
if self.sendButtonHasApplyIcon || self.effectBadgeView != nil {
|
||||
let result = UIView()
|
||||
result.frame = self.bounds
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -3,32 +3,40 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import GlassBackgroundComponent
|
||||
|
||||
private let cancelFont = Font.regular(17.0)
|
||||
|
||||
final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
||||
public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBackgroundView.ContentView {
|
||||
private let cancel: () -> Void
|
||||
|
||||
private let arrowNode: ASImageNode
|
||||
private let arrowView: GlassBackgroundView.ContentImageView
|
||||
private let labelNode: TextNode
|
||||
private let tintLabelNode: TextNode
|
||||
private let cancelButton: HighlightableButtonNode
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private(set) var isDisplayingCancel = false
|
||||
public let tintMask: UIView
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
|
||||
public private(set) var isDisplayingCancel = false
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
|
||||
self.tintMask = UIView()
|
||||
|
||||
self.cancel = cancel
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.isLayerBacked = true
|
||||
self.arrowNode.displayWithoutProcessing = true
|
||||
self.arrowNode.displaysAsynchronously = false
|
||||
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)
|
||||
self.arrowView = GlassBackgroundView.ContentImageView()
|
||||
self.arrowView.image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate)
|
||||
self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.tintLabelNode = TextNode()
|
||||
self.tintLabelNode.displaysAsynchronously = false
|
||||
self.tintLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.cancelButton = HighlightableButtonNode()
|
||||
self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
|
||||
self.cancelButton.alpha = 0.0
|
||||
|
|
@ -37,21 +45,28 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
|||
|
||||
self.strings = strings
|
||||
|
||||
super.init()
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubnode(self.arrowNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubview(self.arrowView)
|
||||
self.tintMask.addSubview(self.arrowView.tintMask)
|
||||
|
||||
self.addSubview(self.labelNode.view)
|
||||
self.tintMask.addSubview(self.tintLabelNode.view)
|
||||
self.addSubnode(self.cancelButton)
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode)
|
||||
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.inputControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let _ = labelApply()
|
||||
let _ = tintLabelApply
|
||||
|
||||
let arrowSize = self.arrowNode.image?.size ?? CGSize()
|
||||
let arrowSize = self.arrowView.image?.size ?? CGSize()
|
||||
let height = max(arrowSize.height, labelLayout.size.height)
|
||||
self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height))
|
||||
self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize)
|
||||
self.arrowView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize)
|
||||
self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: 1.0 + floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
self.tintLabelNode.frame = self.labelNode.frame
|
||||
|
||||
let cancelSize = self.cancelButton.measure(CGSize(width: 200.0, height: 100.0))
|
||||
self.cancelButton.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - cancelSize.width) / 2.0), y: floor((height - cancelSize.height) / 2.0)), size: cancelSize)
|
||||
|
|
@ -59,19 +74,26 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
|||
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)
|
||||
self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
|
||||
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let (_, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let _ = labelApply()
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) {
|
||||
public func updateTheme(theme: PresentationTheme) {
|
||||
self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor
|
||||
self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
|
||||
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode)
|
||||
let (_, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.actionControlForegroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let _ = labelApply()
|
||||
let _ = tintLabelApply()
|
||||
}
|
||||
|
||||
public func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) {
|
||||
if self.isDisplayingCancel != isDisplayingCancel {
|
||||
self.isDisplayingCancel = isDisplayingCancel
|
||||
if isDisplayingCancel {
|
||||
self.arrowNode.alpha = 0.0
|
||||
self.arrowView.alpha = 0.0
|
||||
self.labelNode.alpha = 0.0
|
||||
self.cancelButton.alpha = 1.0
|
||||
|
||||
|
|
@ -85,17 +107,17 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
|||
self.cancelButton.layer.animatePosition(from: CGPoint(x: 0.0, y: 22.0), to: CGPoint(), duration: 0.2, additive: true)
|
||||
self.cancelButton.layer.animateScale(from: 0.25, to: 1.0, duration: 0.25)
|
||||
|
||||
self.arrowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
self.arrowView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
} else {
|
||||
self.arrowNode.alpha = 1.0
|
||||
self.arrowView.alpha = 1.0
|
||||
self.labelNode.alpha = 1.0
|
||||
self.cancelButton.alpha = 0.0
|
||||
|
||||
if animated {
|
||||
self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.arrowView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.cancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
}
|
||||
|
|
@ -103,22 +125,20 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.cancelButton.alpha.isZero, self.cancelButton.frame.insetBy(dx: -5.0, dy: -5.0).contains(point) {
|
||||
return self.cancelButton.view
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
@objc private func cancelPressed() {
|
||||
self.cancel()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
|
||||
public func animateIn() {
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
|
||||
public func animateOut() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -19,9 +19,9 @@ private final class ChatTextInputAudioRecordingTimeNodeParameters: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(15.0)
|
||||
private let textFont = Font.with(size: 15.0, design: .camera)
|
||||
|
||||
final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
||||
public final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
||||
private let textNode: TextNode
|
||||
|
||||
private var timestamp: Double = 0.0 {
|
||||
|
|
@ -32,9 +32,9 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
|||
private let stateDisposable = MetaDisposable()
|
||||
|
||||
private var didStart = false
|
||||
var started = {}
|
||||
public var started = {}
|
||||
|
||||
var audioRecorder: ManagedAudioRecorder? {
|
||||
public var audioRecorder: ManagedAudioRecorder? {
|
||||
didSet {
|
||||
if self.audioRecorder !== oldValue {
|
||||
if let audioRecorder = self.audioRecorder {
|
||||
|
|
@ -65,7 +65,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
|||
|
||||
private var durationDisposable: MetaDisposable?
|
||||
|
||||
var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
|
||||
public var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
|
||||
didSet {
|
||||
if self.videoRecordingStatus !== oldValue {
|
||||
if self.durationDisposable == nil {
|
||||
|
|
@ -93,7 +93,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
|||
|
||||
private var theme: PresentationTheme
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
public init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.textNode = TextNode()
|
||||
|
|
@ -106,13 +106,13 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
|||
self.durationDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
public func updateTheme(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let makeLayout = TextNode.asyncLayout(self.textNode)
|
||||
let (size, apply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "0:00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let _ = apply()
|
||||
|
|
@ -120,7 +120,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
|
|||
return size.size
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp, theme: self.theme)
|
||||
}
|
||||
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -8,7 +8,7 @@ import TelegramStringFormatting
|
|||
import AppBundle
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
|
||||
public final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
|
||||
private var theme: PresentationTheme
|
||||
private let iconNode: ASImageNode
|
||||
private let iconArrowContainerNode: ASDisplayNode
|
||||
|
|
@ -20,7 +20,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
|
|||
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
public init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
|
@ -52,7 +52,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
|
|||
self.timer?.invalidate()
|
||||
}
|
||||
|
||||
func updateState(_ slowmodeState: ChatSlowmodeState) {
|
||||
public func updateState(_ slowmodeState: ChatSlowmodeState) {
|
||||
if self.slowmodeState != slowmodeState {
|
||||
self.slowmodeState = slowmodeState
|
||||
self.update()
|
||||
|
|
@ -97,7 +97,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
public func updateLayout(size: CGSize) {
|
||||
self.validLayout = size
|
||||
|
||||
var leftInset: CGFloat = 0.0
|
||||
|
|
@ -24,6 +24,8 @@ swift_library(
|
|||
"//submodules/Components/LottieAnimationComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import ComponentFlow
|
|||
import LottieAnimationComponent
|
||||
import LottieComponent
|
||||
import LegacyInstantVideoController
|
||||
import GlassBackgroundComponent
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
private let offsetThreshold: CGFloat = 10.0
|
||||
private let dismissOffsetThreshold: CGFloat = 70.0
|
||||
|
|
@ -453,17 +455,23 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM
|
|||
self.updateAnimation(previousMode: self.mode)
|
||||
|
||||
self.pallete = legacyInputMicPalette(from: theme)
|
||||
self.micDecorationValue?.setColor( self.theme.chat.inputPanel.actionControlFillColor)
|
||||
self.micDecorationValue?.setColor(self.theme.chat.inputPanel.actionControlFillColor)
|
||||
(self.micLockValue as? LockView)?.updateTheme(theme)
|
||||
}
|
||||
|
||||
public override func createLockPanelView() -> UIView! {
|
||||
public override func createLockPanelView() -> (UIView & TGModernConversationInputMicButtonLockPanelView)! {
|
||||
let isDark: Bool
|
||||
let tintColor: UIColor
|
||||
if self.hidesOnLock {
|
||||
let view = WrapperBlurrredBackgroundView(frame: CGRect(origin: .zero, size: CGSize(width: 40.0, height: 72.0)))
|
||||
return view
|
||||
isDark = false
|
||||
tintColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
} else {
|
||||
return super.createLockPanelView()
|
||||
isDark = self.theme.overallDarkAppearance
|
||||
tintColor = self.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65)
|
||||
}
|
||||
|
||||
let view = WrapperBlurrredBackgroundView(size: CGSize(width: 40.0, height: 72.0), isDark: isDark, tintColor: tintColor)
|
||||
return view
|
||||
}
|
||||
|
||||
public func cancelRecording() {
|
||||
|
|
@ -614,16 +622,22 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM
|
|||
}
|
||||
}
|
||||
|
||||
private class WrapperBlurrredBackgroundView: UIView {
|
||||
let view: BlurredBackgroundView
|
||||
private class WrapperBlurrredBackgroundView: UIView, TGModernConversationInputMicButtonLockPanelView {
|
||||
let isDark: Bool
|
||||
let glassTintColor: UIColor
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let view = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
|
||||
view.frame = CGRect(origin: .zero, size: frame.size)
|
||||
view.update(size: frame.size, cornerRadius: frame.width / 2.0, transition: .immediate)
|
||||
let view: GlassBackgroundView
|
||||
|
||||
init(size: CGSize, isDark: Bool, tintColor: UIColor) {
|
||||
self.isDark = isDark
|
||||
self.glassTintColor = tintColor
|
||||
|
||||
let view = GlassBackgroundView()
|
||||
view.frame = CGRect(origin: CGPoint(), size: size)
|
||||
view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: .immediate)
|
||||
self.view = view
|
||||
|
||||
super.init(frame: frame)
|
||||
super.init(frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
|
@ -637,7 +651,14 @@ private class WrapperBlurrredBackgroundView: UIView {
|
|||
return super.frame
|
||||
} set {
|
||||
super.frame = newValue
|
||||
self.view.update(size: newValue.size, cornerRadius: newValue.width / 2.0, transition: .immediate)
|
||||
self.view.frame = CGRect(origin: CGPoint(), size: newValue.size)
|
||||
self.view.update(size: newValue.size, cornerRadius: min(newValue.width, newValue.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ size: CGSize) {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateFrame(view: self.view, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: ComponentTransition(transition))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -387,12 +387,16 @@ public final class GlassBackgroundView: UIView {
|
|||
if let nativeView = self.nativeView {
|
||||
let previousFrame = nativeView.frame
|
||||
|
||||
transition.containedViewLayoutTransition.animateView {
|
||||
if transition.animation.isImmediate {
|
||||
nativeView.layer.cornerRadius = cornerRadius
|
||||
nativeView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
} else {
|
||||
transition.containedViewLayoutTransition.animateView {
|
||||
nativeView.layer.cornerRadius = cornerRadius
|
||||
nativeView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
nativeView.layer.animateFrame(from: previousFrame, to: CGRect(origin: CGPoint(), size: size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
nativeView.layer.animateFrame(from: previousFrame, to: CGRect(origin: CGPoint(), size: size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
backgroundNode.updateColor(color: .clear, forceKeepBlur: tintColor.alpha != 1.0, transition: transition.containedViewLayoutTransition)
|
||||
|
|
|
|||
|
|
@ -1826,7 +1826,8 @@ public class TrimView: UIView {
|
|||
endPosition: Double,
|
||||
position: Double,
|
||||
minDuration: Double,
|
||||
maxDuration: Double
|
||||
maxDuration: Double,
|
||||
isBorderless: Bool
|
||||
)?
|
||||
|
||||
public func update(
|
||||
|
|
@ -1840,10 +1841,13 @@ public class TrimView: UIView {
|
|||
position: Double,
|
||||
minDuration: Double,
|
||||
maxDuration: Double,
|
||||
isBorderless: Bool = false,
|
||||
transition: ComponentTransition
|
||||
) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) {
|
||||
let isFirstTime = self.params == nil
|
||||
self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration)
|
||||
self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration, isBorderless)
|
||||
|
||||
self.borderView.isHidden = isBorderless
|
||||
|
||||
let effectiveHandleWidth: CGFloat
|
||||
let fullTrackHeight: CGFloat
|
||||
|
|
@ -1891,7 +1895,7 @@ public class TrimView: UIView {
|
|||
case .videoMessage:
|
||||
effectiveHandleWidth = 16.0
|
||||
fullTrackHeight = 33.0
|
||||
capsuleOffset = 8.0
|
||||
capsuleOffset = 10.0
|
||||
color = theme.chat.inputPanel.panelControlAccentColor
|
||||
highlightColor = theme.chat.inputPanel.panelControlAccentColor
|
||||
|
||||
|
|
@ -1918,8 +1922,9 @@ public class TrimView: UIView {
|
|||
capsuleOffset = 8.0
|
||||
color = theme.chat.inputPanel.panelControlAccentColor
|
||||
highlightColor = theme.chat.inputPanel.panelControlAccentColor
|
||||
self.borderView.isHidden = true
|
||||
|
||||
self.zoneView.backgroundColor = UIColor(white: 1.0, alpha: 0.4)
|
||||
self.zoneView.backgroundColor = .clear
|
||||
|
||||
if isFirstTime {
|
||||
self.borderView.image = generateImage(CGSize(width: 3.0, height: fullTrackHeight), rotatedContext: { size, context in
|
||||
|
|
@ -1941,6 +1946,9 @@ public class TrimView: UIView {
|
|||
self.leftHandleView.image = handleImage
|
||||
self.rightHandleView.image = handleImage
|
||||
|
||||
self.leftHandleView.image = nil
|
||||
self.rightHandleView.image = nil
|
||||
|
||||
self.leftCapsuleView.backgroundColor = .white
|
||||
self.rightCapsuleView.backgroundColor = .white
|
||||
}
|
||||
|
|
@ -1972,7 +1980,7 @@ public class TrimView: UIView {
|
|||
rightHandleFrame.origin.x = min(rightHandleFrame.origin.x, totalWidth - visualInsets.right - effectiveHandleWidth)
|
||||
transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame)
|
||||
|
||||
let capsuleSize = CGSize(width: 2.0, height: 11.0)
|
||||
let capsuleSize = CGSize(width: 3.0, height: 12.0)
|
||||
transition.setFrame(view: self.leftCapsuleView, frame: CGRect(origin: CGPoint(x: capsuleOffset, y: floorToScreenPixels((leftHandleFrame.height - capsuleSize.height) / 2.0)), size: capsuleSize))
|
||||
transition.setFrame(view: self.rightCapsuleView, frame: CGRect(origin: CGPoint(x: capsuleOffset, y: floorToScreenPixels((leftHandleFrame.height - capsuleSize.height) / 2.0)), size: capsuleSize))
|
||||
|
||||
|
|
|
|||
|
|
@ -442,6 +442,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
|||
}, dismissUrlPreview: {
|
||||
}, dismissForwardMessages: {
|
||||
}, dismissSuggestPost: {
|
||||
}, displayUndo: { _ in
|
||||
}, sendEmoji: { _, _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
|
|
|||
|
|
@ -829,6 +829,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
|||
}, dismissUrlPreview: {
|
||||
}, dismissForwardMessages: {
|
||||
}, dismissSuggestPost: {
|
||||
}, displayUndo: { _ in
|
||||
}, sendEmoji: { _, _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
|
|
|||
|
|
@ -3932,7 +3932,7 @@ extension ChatControllerImpl {
|
|||
|
||||
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
||||
|
||||
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true)
|
||||
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true)
|
||||
contextController.dismissed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
||||
|
|
@ -4364,6 +4364,16 @@ extension ChatControllerImpl {
|
|||
}
|
||||
return state
|
||||
})
|
||||
}, displayUndo: { [weak self] content in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controllerInteraction?.displayUndo(content)
|
||||
}, sendEmoji: { [weak self] text, attribute, immediately in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controllerInteraction?.sendEmoji(text, attribute, immediately)
|
||||
}, updateHistoryFilter: { [weak self] update in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -9961,7 +9961,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
transition.updateSublayerTransformScale(node: self.chatDisplayNode.historyNode, scale: scale)
|
||||
}
|
||||
|
||||
func restrictedSendingContentsText() -> String {
|
||||
public func restrictedSendingContentsText() -> String {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return self.presentationData.strings.Chat_SendNotAllowedText
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import SpaceWarpView
|
|||
import ChatSideTopicsPanel
|
||||
import GlassBackgroundComponent
|
||||
import ChatThemeScreen
|
||||
import ChatTextInputPanelNode
|
||||
|
||||
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
|
||||
let itemNode: OverlayMediaItemNode
|
||||
|
|
@ -236,6 +237,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
private var leftPanelContainer: ChatControllerTitlePanelNodeContainer
|
||||
private(set) var leftPanel: (component: AnyComponentWithIdentity<ChatSidePanelEnvironment>, view: ComponentView<ChatSidePanelEnvironment>)?
|
||||
|
||||
private var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
|
||||
|
||||
private var inputPanelBackgroundBlurMask: UIImageView?
|
||||
private var inputPanelBackgroundBlurView: VariableBlurView?
|
||||
|
||||
|
|
@ -855,6 +858,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in
|
||||
self?.interfaceInteraction?.presentController(controller, nil)
|
||||
})
|
||||
self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel
|
||||
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage
|
||||
self.textInputPanelNode?.updateHeight = { [weak self] animated in
|
||||
if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight {
|
||||
|
|
@ -2203,6 +2207,32 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
if "".isEmpty {
|
||||
var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
|
||||
if let current = self.bottomBackgroundEdgeEffectNode {
|
||||
bottomBackgroundEdgeEffectNode = current
|
||||
} else {
|
||||
if let value = self.backgroundNode.makeEdgeEffectNode() {
|
||||
bottomBackgroundEdgeEffectNode = value
|
||||
self.bottomBackgroundEdgeEffectNode = value
|
||||
self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view)
|
||||
}
|
||||
}
|
||||
|
||||
if let bottomBackgroundEdgeEffectNode {
|
||||
var blurFrame = inputBackgroundFrame
|
||||
blurFrame.origin.y -= 26.0
|
||||
blurFrame.size.height += 100.0
|
||||
transition.updateFrame(node: bottomBackgroundEdgeEffectNode, frame: blurFrame)
|
||||
bottomBackgroundEdgeEffectNode.update(
|
||||
rect: blurFrame,
|
||||
edge: WallpaperEdgeEffectEdge(edge: .bottom, size: 80.0),
|
||||
containerSize: wallpaperBounds.size,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !"".isEmpty {
|
||||
let blurView: VariableBlurView
|
||||
let blurMask: UIImageView
|
||||
|
|
@ -3931,9 +3961,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
func sendButtonFrame() -> CGRect? {
|
||||
if let mediaPreviewNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode {
|
||||
return mediaPreviewNode.convert(mediaPreviewNode.sendButton.frame, to: self)
|
||||
} else if let frame = self.textInputPanelNode?.actionButtons.frame {
|
||||
if let frame = self.textInputPanelNode?.actionButtons.frame {
|
||||
return self.textInputPanelNode?.convert(frame, to: self)
|
||||
} else {
|
||||
return nil
|
||||
|
|
@ -3974,10 +4002,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
return textInputPanelNode.frameForInputActionButton().flatMap {
|
||||
return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY)
|
||||
}
|
||||
} else if let recordingPreviewPanelNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode {
|
||||
return recordingPreviewPanelNode.frameForInputActionButton().flatMap {
|
||||
return $0.offsetBy(dx: recordingPreviewPanelNode.frame.minX, dy: recordingPreviewPanelNode.frame.minY)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4722,7 +4746,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() {
|
||||
usedCorrelationId = correlationId
|
||||
let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: textInput, replyPanel: replyPanel)
|
||||
let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: ChatMessageTransitionNodeImpl.Source.TextInput(
|
||||
backgroundView: textInput.backgroundView,
|
||||
contentView: textInput.contentView,
|
||||
sourceRect: textInput.sourceRect,
|
||||
scrollOffset: textInput.scrollOffset
|
||||
), replyPanel: replyPanel)
|
||||
self.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: {
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ChatPresentationInterfaceState
|
|||
import SwiftSignalKit
|
||||
import TextFormat
|
||||
import ChatContextQuery
|
||||
import ChatTextInputPanelNode
|
||||
|
||||
func serviceTasksForChatPresentationIntefaceState(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, updateState: @escaping ((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void) -> [AnyHashable: () -> Disposable] {
|
||||
var missingEmoji = Set<Int64>()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ChatInputNode
|
|||
import ChatEntityKeyboardInputNode
|
||||
import ChatInputPanelNode
|
||||
import ChatButtonKeyboardInputNode
|
||||
import ChatTextInputPanelNode
|
||||
|
||||
func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? {
|
||||
if let inputPanelNode = inputPanelNode, !(inputPanelNode is ChatTextInputPanelNode) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import ChatBotStartInputPanelNode
|
|||
import ChatChannelSubscriberInputPanelNode
|
||||
import ChatMessageSelectionInputPanelNode
|
||||
import ChatControllerInteraction
|
||||
import ChatTextInputPanelNode
|
||||
|
||||
func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) {
|
||||
if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
|
||||
|
|
@ -418,12 +419,11 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||
}
|
||||
}
|
||||
|
||||
var displayBotStartPanel = false
|
||||
|
||||
var isScheduledMessages = false
|
||||
if case .scheduledMessages = chatPresentationInterfaceState.subject {
|
||||
isScheduledMessages = true
|
||||
}
|
||||
var displayBotStartPanel = false
|
||||
|
||||
if !isScheduledMessages {
|
||||
if let _ = chatPresentationInterfaceState.botStartPayload {
|
||||
|
|
@ -436,33 +436,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||
}
|
||||
}
|
||||
}
|
||||
let _ = displayBotStartPanel
|
||||
|
||||
if displayBotStartPanel, !"".isEmpty {
|
||||
if let currentPanel = (currentPanel as? ChatBotStartInputPanelNode) ?? (currentSecondaryPanel as? ChatBotStartInputPanelNode) {
|
||||
currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||
return (currentPanel, nil)
|
||||
} else {
|
||||
let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||
panel.context = context
|
||||
panel.chatControllerInteraction = chatControllerInteraction
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
return (panel, nil)
|
||||
}
|
||||
} else {
|
||||
if let _ = chatPresentationInterfaceState.interfaceState.mediaDraftState {
|
||||
if let currentPanel = (currentPanel as? ChatRecordingPreviewInputPanelNode) ?? (currentSecondaryPanel as? ChatRecordingPreviewInputPanelNode) {
|
||||
return (currentPanel, nil)
|
||||
} else {
|
||||
let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme)
|
||||
panel.context = context
|
||||
panel.chatControllerInteraction = chatControllerInteraction
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
return (panel, nil)
|
||||
}
|
||||
}
|
||||
|
||||
displayInputTextPanel = true
|
||||
}
|
||||
displayInputTextPanel = true
|
||||
}
|
||||
|
||||
if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
|
||||
|
|
@ -504,7 +480,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||
let panel = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in
|
||||
interfaceInteraction?.presentController(controller, nil)
|
||||
})
|
||||
|
||||
panel.textInputAccessoryPanel = textInputAccessoryPanel
|
||||
panel.chatControllerInteraction = chatControllerInteraction
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
panel.context = context
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -81,6 +81,25 @@ public enum WallpaperDisplayMode {
|
|||
}
|
||||
}
|
||||
|
||||
public struct WallpaperEdgeEffectEdge: Equatable {
|
||||
public enum Edge {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
public var edge: Edge
|
||||
public var size: CGFloat
|
||||
|
||||
public init(edge: Edge, size: CGFloat) {
|
||||
self.edge = edge
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
public protocol WallpaperEdgeEffectNode: ASDisplayNode {
|
||||
func update(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
|
||||
public protocol WallpaperBackgroundNode: ASDisplayNode {
|
||||
var isReady: Signal<Bool, NoError> { get }
|
||||
var rotation: CGFloat { get set }
|
||||
|
|
@ -99,6 +118,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode {
|
|||
func hasExtraBubbleBackground() -> Bool
|
||||
|
||||
func makeDimmedNode() -> ASDisplayNode?
|
||||
|
||||
func makeEdgeEffectNode() -> WallpaperEdgeEffectNode?
|
||||
}
|
||||
|
||||
private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOverlayLayer {
|
||||
|
|
@ -493,7 +514,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
|
|||
|
||||
if needsGradientBackground, let gradientBackgroundNode = gradientBackgroundSource {
|
||||
if self.gradientWallpaperNode == nil {
|
||||
let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
|
||||
let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true)
|
||||
gradientWallpaperNode.frame = self.bounds
|
||||
self.gradientWallpaperNode = gradientWallpaperNode
|
||||
self.insertSubnode(gradientWallpaperNode, at: 0)
|
||||
|
|
@ -765,6 +786,8 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
|
|||
|
||||
private var modelStickerNode: DefaultAnimatedStickerNodeImpl?
|
||||
|
||||
fileprivate let edgeEffectNodes = SparseBag<Weak<WallpaperEdgeEffectNodeImpl>>()
|
||||
|
||||
private var isSettingUpWallpaper: Bool = false
|
||||
|
||||
private struct CachedValidPatternImage {
|
||||
|
|
@ -1188,6 +1211,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
|
|||
self.contentNode.alpha = 1.0
|
||||
self.patternImageLayer.backgroundColor = nil
|
||||
}
|
||||
|
||||
for edgeEffectNode in self.edgeEffectNodes {
|
||||
if let edgeEffectNode = edgeEffectNode.value {
|
||||
edgeEffectNode.updatePattern(isInverted: invertPattern)
|
||||
}
|
||||
}
|
||||
default:
|
||||
self.patternImageDisposable.set(nil)
|
||||
self.symbolImageDisposable.set(nil)
|
||||
|
|
@ -1198,6 +1227,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
|
|||
self.backgroundColor = nil
|
||||
self.gradientBackgroundNode?.contentView.alpha = 1.0
|
||||
self.contentNode.alpha = 1.0
|
||||
|
||||
for edgeEffectNode in self.edgeEffectNodes {
|
||||
if let edgeEffectNode = edgeEffectNode.value {
|
||||
edgeEffectNode.updatePattern(isInverted: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1670,11 +1705,149 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
|
|||
|
||||
public func makeDimmedNode() -> ASDisplayNode? {
|
||||
if let gradientBackgroundNode = self.gradientBackgroundNode {
|
||||
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
|
||||
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func makeEdgeEffectNode() -> WallpaperEdgeEffectNode? {
|
||||
if let gradientBackgroundNode = self.gradientBackgroundNode {
|
||||
let node = WallpaperEdgeEffectNodeImpl(parentNode: self)
|
||||
node.cloneNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: false)
|
||||
return node
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode {
|
||||
var cloneNode: GradientBackgroundNode.CloneNode? {
|
||||
didSet {
|
||||
if self.cloneNode !== oldValue {
|
||||
if let cloneNode = self.cloneNode {
|
||||
self.containerNode.insertSubnode(cloneNode, at: 0)
|
||||
|
||||
if let params = self.params {
|
||||
self.updateImpl(rect: params.rect, edge: params.edge, containerSize: params.containerSize, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Params: Equatable {
|
||||
let rect: CGRect
|
||||
let edge: WallpaperEdgeEffectEdge
|
||||
let containerSize: CGSize
|
||||
|
||||
init(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize) {
|
||||
self.rect = rect
|
||||
self.edge = edge
|
||||
self.containerSize = containerSize
|
||||
}
|
||||
}
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private let containerMaskingNode: ASDisplayNode
|
||||
private let overlayNode: ASDisplayNode
|
||||
private let maskView: UIImageView
|
||||
|
||||
private weak var parentNode: WallpaperBackgroundNodeImpl?
|
||||
private var index: Int?
|
||||
private var params: Params?
|
||||
|
||||
private var isInverted: Bool = false
|
||||
|
||||
init(parentNode: WallpaperBackgroundNodeImpl) {
|
||||
self.parentNode = parentNode
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.anchorPoint = CGPoint()
|
||||
self.containerNode.clipsToBounds = true
|
||||
|
||||
self.containerMaskingNode = ASDisplayNode()
|
||||
self.containerMaskingNode.addSubnode(self.containerNode)
|
||||
|
||||
self.overlayNode = ASDisplayNode()
|
||||
|
||||
self.maskView = UIImageView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.containerMaskingNode)
|
||||
self.containerMaskingNode.view.mask = self.maskView
|
||||
|
||||
self.containerNode.addSubnode(self.overlayNode)
|
||||
|
||||
self.index = parentNode.edgeEffectNodes.add(Weak(self))
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let index = self.index, let parentNode = self.parentNode {
|
||||
parentNode.edgeEffectNodes.remove(index)
|
||||
}
|
||||
}
|
||||
|
||||
func updatePattern(isInverted: Bool) {
|
||||
if self.isInverted != isInverted {
|
||||
self.isInverted = isInverted
|
||||
|
||||
self.overlayNode.backgroundColor = isInverted ? .black : .clear
|
||||
}
|
||||
}
|
||||
|
||||
func update(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let params = Params(rect: rect, edge: edge, containerSize: containerSize)
|
||||
if self.params != params {
|
||||
self.params = params
|
||||
self.updateImpl(rect: params.rect, edge: params.edge, containerSize: params.containerSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateImpl(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.containerMaskingNode, frame: CGRect(origin: CGPoint(), size: rect.size))
|
||||
transition.updateBounds(node: self.containerNode, bounds: CGRect(origin: CGPoint(x: rect.minX, y: rect.minY), size: rect.size))
|
||||
|
||||
if self.maskView.image?.size.height != edge.size {
|
||||
let baseGradientAlpha: CGFloat = 0.75
|
||||
let numSteps = 8
|
||||
let firstStep = 1
|
||||
let firstLocation = 0.0
|
||||
let colors: [UIColor] = (0 ..< numSteps).map { i in
|
||||
if i < firstStep {
|
||||
return UIColor(white: 1.0, alpha: 1.0)
|
||||
} else {
|
||||
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||
let value: CGFloat = bezierPoint(0.42, 0.0, 0.58, 1.0, step)
|
||||
return UIColor(white: 1.0, alpha: baseGradientAlpha * value)
|
||||
}
|
||||
}
|
||||
let locations: [CGFloat] = (0 ..< numSteps).map { i in
|
||||
if i < firstStep {
|
||||
return 0.0
|
||||
} else {
|
||||
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||
return (firstLocation + (1.0 - firstLocation) * step)
|
||||
}
|
||||
}
|
||||
|
||||
self.maskView.image = generateGradientImage(
|
||||
size: CGSize(width: 8.0, height: edge.size),
|
||||
colors: colors,
|
||||
locations: locations
|
||||
)?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(edge.size))
|
||||
}
|
||||
|
||||
transition.updateFrame(view: self.maskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: rect.size))
|
||||
|
||||
transition.updateFrame(node: self.overlayNode, frame: CGRect(origin: CGPoint(), size: containerSize))
|
||||
|
||||
if let cloneNode = self.cloneNode {
|
||||
transition.updateFrame(node: cloneNode, frame: CGRect(origin: CGPoint(), size: containerSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private protocol WallpaperComponentView: AnyObject {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue