mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Various improvements
This commit is contained in:
parent
81d23edd72
commit
c5168b8905
@ -724,15 +724,18 @@ public struct StoryCameraTransitionIn {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceCornerRadius: CGFloat
|
||||
public let useFillAnimation: Bool
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceCornerRadius: CGFloat
|
||||
sourceCornerRadius: CGFloat,
|
||||
useFillAnimation: Bool
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceCornerRadius = sourceCornerRadius
|
||||
self.useFillAnimation = useFillAnimation
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1022,7 +1022,7 @@ public protocol ChatController: ViewController {
|
||||
var parentController: ViewController? { get set }
|
||||
var customNavigationController: NavigationController? { get set }
|
||||
|
||||
var dismissPreviewing: (() -> Void)? { get set }
|
||||
var dismissPreviewing: ((Bool) -> (() -> Void))? { get set }
|
||||
var purposefulAction: (() -> Void)? { get set }
|
||||
|
||||
var stateUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set }
|
||||
|
@ -298,15 +298,17 @@ public final class AvatarNode: ASDisplayNode {
|
||||
let peerId: EnginePeer.Id?
|
||||
let resourceId: String?
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
|
||||
let displayDimensions: CGSize
|
||||
init(
|
||||
peerId: EnginePeer.Id?,
|
||||
resourceId: String?,
|
||||
clipStyle: AvatarNodeClipStyle
|
||||
clipStyle: AvatarNodeClipStyle,
|
||||
displayDimensions: CGSize
|
||||
) {
|
||||
self.peerId = peerId
|
||||
self.resourceId = resourceId
|
||||
self.clipStyle = clipStyle
|
||||
self.displayDimensions = displayDimensions
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,7 +663,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
let params = Params(
|
||||
peerId: peer?.id,
|
||||
resourceId: smallProfileImage?.resource.id.stringRepresentation,
|
||||
clipStyle: clipStyle
|
||||
clipStyle: clipStyle,
|
||||
displayDimensions: displayDimensions
|
||||
)
|
||||
if self.params == params {
|
||||
return
|
||||
|
@ -174,7 +174,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _ ,_ in
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
|
||||
|
@ -1502,7 +1502,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
} else {
|
||||
var dismissPreviewingImpl: (() -> Void)?
|
||||
var dismissPreviewingImpl: ((Bool) -> (() -> Void))?
|
||||
let source: ContextContentSource
|
||||
if let location = location {
|
||||
source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location))
|
||||
@ -1510,8 +1510,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
|
||||
chatController.customNavigationController = strongSelf.navigationController as? NavigationController
|
||||
chatController.canReadHistory.set(false)
|
||||
chatController.dismissPreviewing = {
|
||||
dismissPreviewingImpl?()
|
||||
chatController.dismissPreviewing = { animateIn in
|
||||
return dismissPreviewingImpl?(animateIn) ?? {}
|
||||
}
|
||||
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
|
||||
}
|
||||
@ -1519,8 +1519,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
|
||||
dismissPreviewingImpl = { [weak contextController] in
|
||||
contextController?.dismiss()
|
||||
dismissPreviewingImpl = { [weak self, weak contextController] animateIn in
|
||||
if let self, let contextController {
|
||||
if animateIn {
|
||||
contextController.statusBar.statusBarStyle = .Ignore
|
||||
self.present(contextController, in: .window(.root))
|
||||
return {
|
||||
contextController.dismissNow()
|
||||
}
|
||||
} else {
|
||||
contextController.dismiss()
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
case let .forum(pinnedIndex, _, threadId, _, _):
|
||||
@ -2794,7 +2805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
private weak var storyCameraTooltip: TooltipScreen?
|
||||
fileprivate func openStoryCamera(fromList: Bool) {
|
||||
fileprivate func openStoryCamera(fromList: Bool, gesturePullOffset: CGFloat? = nil) {
|
||||
guard !self.context.isFrozen else {
|
||||
let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)
|
||||
self.push(controller)
|
||||
@ -2920,8 +2931,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
cameraTransitionIn = StoryCameraTransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
sourceRect: gesturePullOffset.flatMap({ transitionView.bounds.offsetBy(dx: -$0, dy: 0) }) ?? transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5,
|
||||
useFillAnimation: gesturePullOffset != nil
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@ -2929,7 +2941,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
cameraTransitionIn = StoryCameraTransitionIn(
|
||||
sourceView: rightButtonView,
|
||||
sourceRect: rightButtonView.bounds,
|
||||
sourceCornerRadius: rightButtonView.bounds.height * 0.5
|
||||
sourceCornerRadius: rightButtonView.bounds.height * 0.5,
|
||||
useFillAnimation: false
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -2983,6 +2996,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
if case .chatList = self.location, let componentView = self.chatListHeaderView() {
|
||||
componentView.storyComposeAction = { [weak self] offset in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openStoryCamera(fromList: true, gesturePullOffset: offset)
|
||||
}
|
||||
|
||||
componentView.storyPeerAction = { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -1748,7 +1748,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
if peer.smallProfileImage != nil && overrideImage == nil {
|
||||
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
|
||||
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: avatarDiameter, height: avatarDiameter))
|
||||
} else {
|
||||
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
|
||||
}
|
||||
|
@ -2184,6 +2184,7 @@ public protocol ContextExtractedContentSource: AnyObject {
|
||||
var adjustContentHorizontally: Bool { get }
|
||||
var adjustContentForSideInset: Bool { get }
|
||||
var ignoreContentTouches: Bool { get }
|
||||
var keepDefaultContentTouches: Bool { get }
|
||||
var blurBackground: Bool { get }
|
||||
var shouldBeDismissed: Signal<Bool, NoError> { get }
|
||||
|
||||
@ -2217,6 +2218,10 @@ public extension ContextExtractedContentSource {
|
||||
var shouldBeDismissed: Signal<Bool, NoError> {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
var keepDefaultContentTouches: Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContextControllerTakeControllerInfo {
|
||||
|
@ -181,6 +181,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
|
||||
func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
guard self.controller.navigationController == nil else {
|
||||
return
|
||||
}
|
||||
self.controller.containerLayoutUpdated(
|
||||
ContainerViewLayout(
|
||||
size: size,
|
||||
@ -376,7 +379,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
if let result = contentNode.containingItem.customHitTest?(contentPoint) {
|
||||
return result
|
||||
} else if let result = contentNode.containingItem.contentHitTest(contentPoint, with: event) {
|
||||
if result is TextSelectionNodeView {
|
||||
if source.keepDefaultContentTouches {
|
||||
return result
|
||||
} else if result is TextSelectionNodeView {
|
||||
return result
|
||||
} else if contentNode.containingItem.contentRect.contains(contentPoint) {
|
||||
return contentNode.containingItem.contentView
|
||||
|
@ -3336,7 +3336,8 @@ public func stickerMediaPickerController(
|
||||
transitionIn: CameraScreenImpl.TransitionIn(
|
||||
sourceView: cameraHolder.parentView,
|
||||
sourceRect: cameraHolder.parentView.bounds,
|
||||
sourceCornerRadius: 0.0
|
||||
sourceCornerRadius: 0.0,
|
||||
useFillAnimation: false
|
||||
),
|
||||
transitionOut: { _ in
|
||||
return CameraScreenImpl.TransitionOut(
|
||||
@ -3453,7 +3454,8 @@ public func avatarMediaPickerController(
|
||||
transitionIn: CameraScreenImpl.TransitionIn(
|
||||
sourceView: cameraHolder.parentView,
|
||||
sourceRect: cameraHolder.parentView.bounds,
|
||||
sourceCornerRadius: 0.0
|
||||
sourceCornerRadius: 0.0,
|
||||
useFillAnimation: false
|
||||
),
|
||||
transitionOut: { _ in
|
||||
return CameraScreenImpl.TransitionOut(
|
||||
|
@ -650,6 +650,9 @@ private extension StarsContext.State.Transaction {
|
||||
if (apiFlags & (1 << 19)) != 0 {
|
||||
flags.insert(.isPaidMessage)
|
||||
}
|
||||
if (apiFlags & (1 << 21)) != 0 {
|
||||
flags.insert(.isBusinessTransfer)
|
||||
}
|
||||
|
||||
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
|
||||
let _ = subscriptionPeriod
|
||||
@ -702,6 +705,7 @@ public final class StarsContext {
|
||||
public static let isReaction = Flags(rawValue: 1 << 5)
|
||||
public static let isStarGiftUpgrade = Flags(rawValue: 1 << 6)
|
||||
public static let isPaidMessage = Flags(rawValue: 1 << 7)
|
||||
public static let isBusinessTransfer = Flags(rawValue: 1 << 8)
|
||||
}
|
||||
|
||||
public enum Peer: Equatable {
|
||||
|
@ -1668,15 +1668,18 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceCornerRadius: CGFloat
|
||||
public let useFillAnimation: Bool
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceCornerRadius: CGFloat
|
||||
sourceCornerRadius: CGFloat,
|
||||
useFillAnimation: Bool
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceCornerRadius = sourceCornerRadius
|
||||
self.useFillAnimation = useFillAnimation
|
||||
}
|
||||
}
|
||||
|
||||
@ -2505,55 +2508,100 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
|
||||
if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
|
||||
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
|
||||
if case .story = controller.mode {
|
||||
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
|
||||
if transitionIn.useFillAnimation {
|
||||
self.backgroundView.alpha = 1.0
|
||||
self.backgroundView.layer.removeAllAnimations()
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.transitionDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15)
|
||||
|
||||
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
|
||||
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animate(
|
||||
from: minSide / 2.0 as NSNumber,
|
||||
to: self.previewContainerView.layer.cornerRadius as NSNumber,
|
||||
keyPath: "cornerRadius",
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
duration: 0.3
|
||||
)
|
||||
} else {
|
||||
self.mainPreviewAnimationWrapperView.bounds = self.mainPreviewView.bounds
|
||||
self.mainPreviewAnimationWrapperView.center = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
|
||||
let transitionMaskView = UIView()
|
||||
transitionMaskView.frame = self.view.bounds
|
||||
self.view.mask = transitionMaskView
|
||||
|
||||
self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
|
||||
let transitionCircleLayer = SimpleShapeLayer()
|
||||
transitionCircleLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 320.0, height: 320.0)), transform: nil)
|
||||
transitionCircleLayer.fillColor = UIColor.white.cgColor
|
||||
transitionCircleLayer.frame = CGSize(width: 320.0, height: 320.0).centered(in: sourceLocalFrame)
|
||||
transitionMaskView.layer.addSublayer(transitionCircleLayer)
|
||||
|
||||
let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView)
|
||||
let sourceCenter = sourceInnerFrame.center
|
||||
self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
|
||||
})
|
||||
let colorFillView = UIView()
|
||||
colorFillView.backgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor
|
||||
colorFillView.frame = self.view.bounds
|
||||
colorFillView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.view.addSubview(colorFillView)
|
||||
|
||||
var sourceBounds = self.mainPreviewView.bounds
|
||||
if let holder = controller.holder {
|
||||
sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size))
|
||||
let iconLayer = SimpleLayer()
|
||||
iconLayer.contents = generateAddIcon(color: self.presentationData.theme.list.itemCheckColors.foregroundColor)?.cgImage
|
||||
iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
|
||||
iconLayer.position = sourceLocalFrame.center
|
||||
colorFillView.layer.addSublayer(iconLayer)
|
||||
|
||||
let labelLayer = SimpleLayer()
|
||||
if let image = generateAddLabel(strings: self.presentationData.strings, color: self.presentationData.theme.list.itemCheckColors.foregroundColor) {
|
||||
labelLayer.contents = image.cgImage
|
||||
labelLayer.bounds = CGRect(origin: .zero, size: image.size)
|
||||
labelLayer.position = CGPoint(x: sourceLocalFrame.center.x, y: sourceLocalFrame.center.y + 43.0 - UIScreenPixel)
|
||||
colorFillView.layer.addSublayer(labelLayer)
|
||||
}
|
||||
self.mainPreviewAnimationWrapperView.layer.animateBounds(from: sourceBounds, to: self.mainPreviewView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
let sourceScale = max(sourceInnerFrame.width / self.previewContainerView.frame.width, sourceInnerFrame.height / self.previewContainerView.frame.height)
|
||||
self.mainPreviewView.transform = CGAffineTransform.identity
|
||||
self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.mainPreviewContainerView.addSubview(self.mainPreviewView)
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.animatedIn = true
|
||||
}
|
||||
iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false)
|
||||
labelLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false)
|
||||
|
||||
transitionCircleLayer.animateScale(from: sourceLocalFrame.width / 320.0, to: 6.0, duration: 0.6, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
self.view.mask = nil
|
||||
colorFillView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
if case .story = controller.mode {
|
||||
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
|
||||
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animate(
|
||||
from: minSide / 2.0 as NSNumber,
|
||||
to: self.previewContainerView.layer.cornerRadius as NSNumber,
|
||||
keyPath: "cornerRadius",
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
duration: 0.3
|
||||
)
|
||||
} else {
|
||||
self.mainPreviewAnimationWrapperView.bounds = self.mainPreviewView.bounds
|
||||
self.mainPreviewAnimationWrapperView.center = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
|
||||
|
||||
self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
|
||||
|
||||
let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView)
|
||||
let sourceCenter = sourceInnerFrame.center
|
||||
self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
|
||||
})
|
||||
|
||||
var sourceBounds = self.mainPreviewView.bounds
|
||||
if let holder = controller.holder {
|
||||
sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size))
|
||||
}
|
||||
self.mainPreviewAnimationWrapperView.layer.animateBounds(from: sourceBounds, to: self.mainPreviewView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
let sourceScale = max(sourceInnerFrame.width / self.previewContainerView.frame.width, sourceInnerFrame.height / self.previewContainerView.frame.height)
|
||||
self.mainPreviewView.transform = CGAffineTransform.identity
|
||||
self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.mainPreviewContainerView.addSubview(self.mainPreviewView)
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.animatedIn = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3767,6 +3815,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateIn() {
|
||||
self.node.animateIn()
|
||||
}
|
||||
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
@ -4009,3 +4061,37 @@ private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoi
|
||||
return position
|
||||
}
|
||||
|
||||
|
||||
private func generateAddIcon(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(3.0)
|
||||
context.setLineCap(.round)
|
||||
|
||||
context.move(to: CGPoint(x: 15.0, y: 5.5))
|
||||
context.addLine(to: CGPoint(x: 15.0, y: 24.5))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 5.5, y: 15.0))
|
||||
context.addLine(to: CGPoint(x: 24.5, y: 15.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? {
|
||||
let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
|
||||
var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
textRect.size.width = ceil(textRect.size.width)
|
||||
textRect.size.height = ceil(textRect.size.height)
|
||||
|
||||
return generateImage(textRect.size, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
titleString.draw(in: textRect)
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
||||
|
@ -1480,6 +1480,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
updatedShareButtonNode.pressed = { [weak strongSelf] in
|
||||
strongSelf?.shareButtonPressed()
|
||||
}
|
||||
updatedShareButtonNode.longPressAction = { [weak strongSelf] node, gesture in
|
||||
strongSelf?.openQuickShare(node: node, gesture: gesture)
|
||||
}
|
||||
}
|
||||
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account)
|
||||
updatedShareButtonNode.frame = CGRect(origin: CGPoint(x: !incoming ? updatedImageFrame.minX - buttonSize.width - 6.0 : updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - buttonSize.height - 4.0 + imageBottomPadding), size: buttonSize)
|
||||
@ -2429,6 +2432,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.displayQuickShare(item.message.id, node, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
private var playedSwipeToReplyHaptic = false
|
||||
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
|
||||
var offset: CGFloat = 0.0
|
||||
|
@ -5804,7 +5804,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.displayQuickShare(node, gesture)
|
||||
item.controllerInteraction.displayQuickShare(item.message.id, node, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1012,6 +1012,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
updatedShareButtonNode.pressed = { [weak strongSelf] in
|
||||
strongSelf?.shareButtonPressed()
|
||||
}
|
||||
updatedShareButtonNode.longPressAction = { [weak strongSelf] node, gesture in
|
||||
strongSelf?.openQuickShare(node: node, gesture: gesture)
|
||||
}
|
||||
}
|
||||
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account)
|
||||
let shareButtonFrame = CGRect(origin: CGPoint(x: baseShareButtonFrame.minX, y: baseShareButtonFrame.maxY - buttonSize.height), size: buttonSize)
|
||||
@ -1546,6 +1549,12 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.displayQuickShare(item.message.id, node, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
private var playedSwipeToReplyHaptic = false
|
||||
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
|
||||
var offset: CGFloat = 0.0
|
||||
|
@ -645,7 +645,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _ ,_ in
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
@ -500,7 +500,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _ ,_ in
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))
|
||||
|
||||
|
@ -289,7 +289,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
|
||||
backgroundSize.height += verticalInset
|
||||
|
||||
let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
backgroundSize.height += titleLayout.size.height
|
||||
backgroundSize.height += verticalSpacing
|
||||
|
||||
@ -297,7 +297,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
|
||||
backgroundSize.height += subtitleLayout.size.height
|
||||
backgroundSize.height += verticalSpacing + paragraphSpacing
|
||||
|
||||
let infoConstrainedSize = CGSize(width: constrainedWidth * 0.7, height: CGFloat.greatestFiniteMagnitude)
|
||||
let infoConstrainedSize = CGSize(width: floor(constrainedWidth * 0.7), height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
var maxTitleWidth: CGFloat = 0.0
|
||||
var maxValueWidth: CGFloat = 0.0
|
||||
@ -389,7 +389,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
|
||||
groupsValueLayoutAndApply = nil
|
||||
}
|
||||
|
||||
backgroundSize.width = horizontalContentInset * 2.0 + maxTitleWidth + attributeSpacing + maxValueWidth
|
||||
backgroundSize.width = horizontalContentInset * 2.0 + max(titleLayout.size.width, maxTitleWidth + attributeSpacing + maxValueWidth)
|
||||
|
||||
let disclaimerText: NSMutableAttributedString
|
||||
if let verification = item.verification {
|
||||
|
@ -24,6 +24,7 @@ swift_library(
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -8,27 +8,38 @@ import TelegramCore
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import LottieComponent
|
||||
import AccountContext
|
||||
import ViewControllerComponent
|
||||
import AvatarNode
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
private let largeCircleSize: CGFloat = 16.0
|
||||
private let smallCircleSize: CGFloat = 8.0
|
||||
|
||||
private final class QuickShareScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let sourceNode: ASDisplayNode
|
||||
let gesture: ContextGesture
|
||||
let openPeer: (EnginePeer.Id) -> Void
|
||||
let completion: (EnginePeer.Id) -> Void
|
||||
let ready: Promise<Bool>
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
sourceNode: ASDisplayNode,
|
||||
gesture: ContextGesture
|
||||
gesture: ContextGesture,
|
||||
openPeer: @escaping (EnginePeer.Id) -> Void,
|
||||
completion: @escaping (EnginePeer.Id) -> Void,
|
||||
ready: Promise<Bool>
|
||||
) {
|
||||
self.context = context
|
||||
self.sourceNode = sourceNode
|
||||
self.gesture = gesture
|
||||
self.openPeer = openPeer
|
||||
self.completion = completion
|
||||
self.ready = ready
|
||||
}
|
||||
|
||||
static func ==(lhs: QuickShareScreenComponent, rhs: QuickShareScreenComponent) -> Bool {
|
||||
@ -36,10 +47,16 @@ private final class QuickShareScreenComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundShadowLayer: SimpleLayer
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let backgroundTintView: UIView
|
||||
private let containerView: UIView
|
||||
|
||||
private let largeCircleLayer: SimpleLayer
|
||||
private let largeCircleShadowLayer: SimpleLayer
|
||||
private let smallCircleLayer: SimpleLayer
|
||||
private let smallCircleShadowLayer: SimpleLayer
|
||||
|
||||
private var items: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
@ -55,12 +72,30 @@ private final class QuickShareScreenComponent: Component {
|
||||
private var initialContinueGesturePoint: CGPoint?
|
||||
private var didMoveFromInitialGesturePoint = false
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
self.backgroundView.clipsToBounds = true
|
||||
self.backgroundTintView = UIView()
|
||||
self.backgroundTintView.clipsToBounds = true
|
||||
|
||||
self.backgroundShadowLayer = SimpleLayer()
|
||||
self.backgroundShadowLayer.opacity = 0.0
|
||||
|
||||
self.largeCircleLayer = SimpleLayer()
|
||||
self.largeCircleShadowLayer = SimpleLayer()
|
||||
self.smallCircleLayer = SimpleLayer()
|
||||
self.smallCircleShadowLayer = SimpleLayer()
|
||||
|
||||
self.largeCircleLayer.backgroundColor = UIColor.black.cgColor
|
||||
self.largeCircleLayer.masksToBounds = true
|
||||
self.largeCircleLayer.cornerRadius = largeCircleSize / 2.0
|
||||
|
||||
self.smallCircleLayer.backgroundColor = UIColor.black.cgColor
|
||||
self.smallCircleLayer.masksToBounds = true
|
||||
self.smallCircleLayer.cornerRadius = smallCircleSize / 2.0
|
||||
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
|
||||
@ -68,6 +103,7 @@ private final class QuickShareScreenComponent: Component {
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.addSubview(self.backgroundTintView)
|
||||
self.layer.addSublayer(self.backgroundShadowLayer)
|
||||
self.addSubview(self.containerView)
|
||||
}
|
||||
|
||||
@ -80,9 +116,40 @@ private final class QuickShareScreenComponent: Component {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.hapticFeedback.impact()
|
||||
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
transition.animateBoundsSize(view: self.backgroundView, from: CGSize(width: 0.0, height: self.backgroundView.bounds.height), to: self.backgroundView.bounds.size)
|
||||
transition.animateBounds(view: self.containerView, from: CGRect(x: self.containerView.bounds.width / 2.0, y: 0.0, width: 0.0, height: self.backgroundView.bounds.height), to: self.containerView.bounds)
|
||||
self.backgroundView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1)
|
||||
self.backgroundTintView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundTintView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1)
|
||||
|
||||
self.backgroundShadowLayer.opacity = 1.0
|
||||
transition.animateBoundsSize(layer: self.backgroundShadowLayer, from: CGSize(width: 0.0, height: self.backgroundShadowLayer.bounds.height), to: self.backgroundShadowLayer.bounds.size)
|
||||
self.backgroundShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
let mainCircleDelay: Double = 0.01
|
||||
let backgroundCenter = self.backgroundView.frame.width / 2.0
|
||||
let backgroundWidth = self.backgroundView.frame.width
|
||||
for item in self.items.values {
|
||||
guard let itemView = item.view else {
|
||||
continue
|
||||
}
|
||||
|
||||
let distance = abs(itemView.frame.center.x - backgroundCenter)
|
||||
let distanceNorm = distance / backgroundWidth
|
||||
let adjustedDistanceNorm = distanceNorm
|
||||
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.3
|
||||
|
||||
itemView.isHidden = true
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemView] in
|
||||
guard let itemView else {
|
||||
return
|
||||
}
|
||||
itemView.isHidden = false
|
||||
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 0.63 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
})
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.containerView.clipsToBounds = false
|
||||
@ -98,21 +165,45 @@ private final class QuickShareScreenComponent: Component {
|
||||
}
|
||||
|
||||
func highlightGestureMoved(location: CGPoint) {
|
||||
var selectedPeerId: EnginePeer.Id?
|
||||
for (peerId, view) in self.items {
|
||||
guard let view = view.view else {
|
||||
continue
|
||||
}
|
||||
if view.frame.contains(location) {
|
||||
self.selectedPeerId = peerId
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
if view.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) {
|
||||
selectedPeerId = peerId
|
||||
break
|
||||
}
|
||||
}
|
||||
if let selectedPeerId, selectedPeerId != self.selectedPeerId {
|
||||
self.hapticFeedback.tap()
|
||||
}
|
||||
self.selectedPeerId = selectedPeerId
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
func highlightGestureFinished(performAction: Bool) {
|
||||
if let selectedPeerId = self.selectedPeerId, performAction {
|
||||
let _ = selectedPeerId
|
||||
if let component = self.component, let peer = self.peers?.first(where: { $0.id == selectedPeerId }), let view = self.items[selectedPeerId]?.view as? ItemComponent.View, let controller = self.environment?.controller() {
|
||||
controller.window?.forEachController({ controller in
|
||||
if let controller = controller as? QuickShareToastScreen {
|
||||
controller.dismiss()
|
||||
}
|
||||
})
|
||||
let toastScreen = QuickShareToastScreen(
|
||||
context: component.context,
|
||||
peer: peer,
|
||||
sourceFrame: view.convert(view.bounds, to: nil),
|
||||
action: {
|
||||
component.openPeer(peer.id)
|
||||
}
|
||||
)
|
||||
controller.present(toastScreen, in: .window(.root))
|
||||
view.avatarNode.isHidden = true
|
||||
|
||||
component.completion(peer.id)
|
||||
}
|
||||
|
||||
self.animateOut {
|
||||
if let controller = self.environment?.controller() {
|
||||
controller.dismiss()
|
||||
@ -158,6 +249,7 @@ private final class QuickShareScreenComponent: Component {
|
||||
} else {
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
component.ready.set(.single(true))
|
||||
})
|
||||
|
||||
component.gesture.externalUpdated = { [weak self] view, point in
|
||||
@ -216,7 +308,19 @@ private final class QuickShareScreenComponent: Component {
|
||||
let padding: CGFloat = 5.0
|
||||
let spacing: CGFloat = 7.0
|
||||
let itemSize = CGSize(width: 38.0, height: 38.0)
|
||||
let itemsCount = 5
|
||||
let selectedItemSize = CGSize(width: 60.0, height: 60.0)
|
||||
let itemsCount = self.peers?.count ?? 5
|
||||
|
||||
let widthExtension: CGFloat = self.selectedPeerId != nil ? selectedItemSize.width - itemSize.width : 0.0
|
||||
|
||||
let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0 + widthExtension, height: itemSize.height + padding * 2.0)
|
||||
let contentRect = CGRect(
|
||||
origin: CGPoint(
|
||||
x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)),
|
||||
y: sourceRect.minY - size.height - padding * 2.0
|
||||
),
|
||||
size: size
|
||||
)
|
||||
|
||||
var itemFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: itemSize)
|
||||
if let peers = self.peers {
|
||||
@ -236,13 +340,18 @@ private final class QuickShareScreenComponent: Component {
|
||||
isFocused = peer.id == selectedPeerId
|
||||
}
|
||||
|
||||
let effectiveItemSize = isFocused == true ? selectedItemSize : itemSize
|
||||
let effectiveItemFrame = CGRect(origin: itemFrame.origin.offsetBy(dx: 0.0, dy: itemSize.height - effectiveItemSize.height), size: effectiveItemSize)
|
||||
|
||||
let _ = componentView.update(
|
||||
transition: componentTransition,
|
||||
component: AnyComponent(
|
||||
ItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
peer: peer,
|
||||
safeInsets: UIEdgeInsets(top: 0.0, left: contentRect.minX + effectiveItemFrame.minX, bottom: 0.0, right: availableSize.width - contentRect.maxX + contentRect.width - effectiveItemFrame.maxX),
|
||||
isFocused: isFocused
|
||||
)
|
||||
),
|
||||
@ -253,29 +362,29 @@ private final class QuickShareScreenComponent: Component {
|
||||
if view.superview == nil {
|
||||
self.containerView.addSubview(view)
|
||||
}
|
||||
componentTransition.setFrame(view: view, frame: itemFrame)
|
||||
componentTransition.setScale(view: view, scale: effectiveItemSize.width / selectedItemSize.width)
|
||||
componentTransition.setBounds(view: view, bounds: CGRect(origin: .zero, size: selectedItemSize))
|
||||
componentTransition.setPosition(view: view, position: effectiveItemFrame.center)
|
||||
}
|
||||
itemFrame.origin.x += itemSize.width + spacing
|
||||
itemFrame.origin.x += effectiveItemFrame.width + spacing
|
||||
}
|
||||
}
|
||||
|
||||
let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0, height: itemSize.height + padding * 2.0)
|
||||
let contentRect = CGRect(
|
||||
origin: CGPoint(
|
||||
x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)),
|
||||
y: sourceRect.minY - size.height - padding * 2.0
|
||||
),
|
||||
size: size
|
||||
)
|
||||
|
||||
self.containerView.layer.cornerRadius = size.height / 2.0
|
||||
self.backgroundView.layer.cornerRadius = size.height / 2.0
|
||||
self.backgroundTintView.layer.cornerRadius = size.height / 2.0
|
||||
transition.setFrame(view: self.backgroundView, frame: contentRect)
|
||||
transition.setFrame(view: self.containerView, frame: contentRect)
|
||||
self.backgroundView.update(size: contentRect.size, cornerRadius: size.height / 2.0, transition: transition.containedViewLayoutTransition)
|
||||
self.backgroundView.update(size: contentRect.size, cornerRadius: 0.0, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.backgroundTintView, frame: CGRect(origin: .zero, size: contentRect.size))
|
||||
|
||||
let shadowInset: CGFloat = 15.0
|
||||
let shadowColor = UIColor(white: 0.0, alpha: 0.4)
|
||||
if self.backgroundShadowLayer.contents == nil, let image = generateBubbleShadowImage(shadow: shadowColor, diameter: 46.0, shadowBlur: shadowInset) {
|
||||
ASDisplayNodeSetResizableContents(self.backgroundShadowLayer, image)
|
||||
}
|
||||
transition.setFrame(layer: self.backgroundShadowLayer, frame: contentRect.insetBy(dx: -shadowInset, dy: -shadowInset))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
@ -293,17 +402,29 @@ public class QuickShareScreen: ViewControllerComponentContainer {
|
||||
private var processedDidAppear: Bool = false
|
||||
private var processedDidDisappear: Bool = false
|
||||
|
||||
private let readyValue = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self.readyValue
|
||||
}
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
sourceNode: ASDisplayNode,
|
||||
gesture: ContextGesture
|
||||
gesture: ContextGesture,
|
||||
openPeer: @escaping (EnginePeer.Id) -> Void,
|
||||
completion: @escaping (EnginePeer.Id) -> Void
|
||||
) {
|
||||
let componentReady = Promise<Bool>()
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: QuickShareScreenComponent(
|
||||
context: context,
|
||||
sourceNode: sourceNode,
|
||||
gesture: gesture
|
||||
gesture: gesture,
|
||||
openPeer: openPeer,
|
||||
completion: completion,
|
||||
ready: componentReady
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
@ -311,6 +432,8 @@ public class QuickShareScreen: ViewControllerComponentContainer {
|
||||
updatedPresentationData: nil
|
||||
)
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
@ -360,18 +483,24 @@ public class QuickShareScreen: ViewControllerComponentContainer {
|
||||
private final class ItemComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let peer: EnginePeer
|
||||
let safeInsets: UIEdgeInsets
|
||||
let isFocused: Bool?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
peer: EnginePeer,
|
||||
safeInsets: UIEdgeInsets,
|
||||
isFocused: Bool?
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.peer = peer
|
||||
self.safeInsets = safeInsets
|
||||
self.isFocused = isFocused
|
||||
}
|
||||
|
||||
@ -379,6 +508,9 @@ private final class ItemComponent: Component {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.safeInsets != rhs.safeInsets {
|
||||
return false
|
||||
}
|
||||
if lhs.isFocused != rhs.isFocused {
|
||||
return false
|
||||
}
|
||||
@ -386,7 +518,7 @@ private final class ItemComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
fileprivate let avatarNode: AvatarNode
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
@ -415,11 +547,13 @@ private final class ItemComponent: Component {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let size = CGSize(width: 60.0, height: 60.0)
|
||||
|
||||
var title = component.peer.compactDisplayTitle
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if component.peer.id == component.context.account.peerId {
|
||||
overrideImage = .savedMessagesIcon
|
||||
title = "Saved Messages"
|
||||
title = component.strings.DialogList_SavedMessages
|
||||
}
|
||||
|
||||
self.avatarNode.setPeer(
|
||||
@ -430,22 +564,16 @@ private final class ItemComponent: Component {
|
||||
synchronousLoad: true
|
||||
)
|
||||
|
||||
self.avatarNode.view.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
|
||||
self.avatarNode.view.bounds = CGRect(origin: .zero, size: availableSize)
|
||||
self.avatarNode.updateSize(size: availableSize)
|
||||
self.avatarNode.view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
self.avatarNode.view.bounds = CGRect(origin: .zero, size: size)
|
||||
self.avatarNode.updateSize(size: size)
|
||||
|
||||
var scale: CGFloat = 1.0
|
||||
var alpha: CGFloat = 1.0
|
||||
var textAlpha: CGFloat = 0.0
|
||||
var textOffset: CGFloat = 6.0
|
||||
if let isFocused = component.isFocused {
|
||||
scale = isFocused ? 1.1 : 1.0
|
||||
alpha = isFocused ? 1.0 : 0.6
|
||||
textAlpha = isFocused ? 1.0 : 0.0
|
||||
textOffset = isFocused ? 0.0 : 6.0
|
||||
}
|
||||
transition.setScale(view: self.avatarNode.view, scale: scale)
|
||||
transition.setAlpha(view: self.avatarNode.view, alpha: alpha)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
@ -459,10 +587,27 @@ private final class ItemComponent: Component {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: -16.0 - textSize.height + textOffset), size: textSize)
|
||||
|
||||
let initialX = floor((size.width - textSize.width) / 2.0)
|
||||
var textFrame = CGRect(origin: CGPoint(x: initialX, y: -13.0 - textSize.height + textOffset), size: textSize)
|
||||
|
||||
let sideInset: CGFloat = 8.0
|
||||
let textPadding: CGFloat = 8.0
|
||||
let leftDistanceToEdge = 0.0 - textFrame.minX
|
||||
let rightDistanceToEdge = textFrame.maxX - size.width
|
||||
|
||||
let leftSafeInset = component.safeInsets.left - textPadding - sideInset
|
||||
let rightSafeInset = component.safeInsets.right - textPadding - sideInset
|
||||
if leftSafeInset < leftDistanceToEdge {
|
||||
textFrame.origin.x = -leftSafeInset
|
||||
}
|
||||
if rightSafeInset < rightDistanceToEdge {
|
||||
textFrame.origin.x = size.width + rightSafeInset - textFrame.width
|
||||
}
|
||||
|
||||
transition.setFrame(view: textView, frame: textFrame)
|
||||
|
||||
let backgroundFrame = textFrame.insetBy(dx: -7.0, dy: -3.0)
|
||||
let backgroundFrame = textFrame.insetBy(dx: -textPadding, dy: -3.0 - UIScreenPixel)
|
||||
transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame)
|
||||
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: .immediate)
|
||||
self.backgroundNode.updateColor(color: component.theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillStatic, enableBlur: true, transition: .immediate)
|
||||
@ -471,7 +616,7 @@ private final class ItemComponent: Component {
|
||||
transition.setAlpha(view: self.backgroundNode.view, alpha: textAlpha)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,3 +628,17 @@ private final class ItemComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(shadow.cgColor)
|
||||
context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
|
||||
context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0))
|
||||
}
|
||||
|
@ -0,0 +1,372 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import AppBundle
|
||||
import ViewControllerComponent
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import AvatarNode
|
||||
import Markdown
|
||||
import LottieComponent
|
||||
|
||||
private final class QuickShareToastScreenComponent: Component {
|
||||
let context: AccountContext
|
||||
let peer: EnginePeer
|
||||
let sourceFrame: CGRect
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peer: EnginePeer,
|
||||
sourceFrame: CGRect,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.peer = peer
|
||||
self.sourceFrame = sourceFrame
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: QuickShareToastScreenComponent, rhs: QuickShareToastScreenComponent) -> Bool {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.sourceFrame != rhs.sourceFrame {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let contentView: UIView
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
|
||||
private let avatarNode: AvatarNode
|
||||
private let animation = ComponentView<Empty>()
|
||||
|
||||
private let content = ComponentView<Empty>()
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
private var component: QuickShareToastScreenComponent?
|
||||
private var environment: EnvironmentType?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var doneTimer: Foundation.Timer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.contentView = UIView()
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.addSubview(self.contentView)
|
||||
self.contentView.addSubview(self.avatarNode.view)
|
||||
|
||||
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture)))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.doneTimer?.invalidate()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.backgroundView.frame.contains(point) {
|
||||
return nil
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc private func tapGesture() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
|
||||
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
|
||||
|
||||
let x1 = sourcePoint.x
|
||||
let y1 = sourcePoint.y
|
||||
let x2 = midPoint.x
|
||||
let y2 = midPoint.y
|
||||
let x3 = targetPosition.x
|
||||
let y3 = targetPosition.y
|
||||
|
||||
var keyframes: [CGPoint] = []
|
||||
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
|
||||
for i in 0 ..< 10 {
|
||||
let k = CGFloat(i) / CGFloat(10 - 1)
|
||||
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
|
||||
keyframes.append(CGPoint(x: x, y: y))
|
||||
}
|
||||
} else {
|
||||
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||
|
||||
for i in 0 ..< 10 {
|
||||
let k = CGFloat(i) / CGFloat(10 - 1)
|
||||
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||
let y = a * x * x + b * x + c
|
||||
keyframes.append(CGPoint(x: x, y: y))
|
||||
}
|
||||
}
|
||||
|
||||
return keyframes
|
||||
}
|
||||
|
||||
let playIconAnimation: (Double) -> Void = { duration in
|
||||
self.avatarNode.contentNode.alpha = 0.0
|
||||
self.avatarNode.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
self.avatarNode.contentNode.layer.animateScale(from: 1.0, to: 0.01, duration: duration, removeOnCompletion: false)
|
||||
|
||||
if let view = self.animation.view as? LottieComponent.View {
|
||||
view.alpha = 1.0
|
||||
view.playOnce()
|
||||
}
|
||||
}
|
||||
|
||||
if component.peer.id == component.context.account.peerId {
|
||||
playIconAnimation(0.2)
|
||||
}
|
||||
|
||||
let offset = self.bounds.height - self.backgroundView.frame.minY
|
||||
self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
if component.peer.id != component.context.account.peerId {
|
||||
playIconAnimation(0.1)
|
||||
}
|
||||
HapticFeedback().success()
|
||||
})
|
||||
|
||||
if let component = self.component {
|
||||
let fromPoint = self.avatarNode.view.convert(component.sourceFrame.center, from: nil).offsetBy(dx: 0.0, dy: -offset)
|
||||
let positionValues = generateAvatarParabollicMotionKeyframes(from: fromPoint, to: .zero, elevation: 20.0)
|
||||
self.avatarNode.layer.animateKeyframes(values: positionValues.map { NSValue(cgPoint: $0) }, duration: 0.35, keyPath: "position", additive: true)
|
||||
self.avatarNode.layer.animateScale(from: component.sourceFrame.width / self.avatarNode.bounds.width, to: 1.0, duration: 0.35)
|
||||
}
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.backgroundView.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func update(component: QuickShareToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
|
||||
if self.component == nil {
|
||||
self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
|
||||
|
||||
let tabBarHeight: CGFloat
|
||||
if !environment.safeInsets.left.isZero {
|
||||
tabBarHeight = 34.0 + environment.safeInsets.bottom
|
||||
} else {
|
||||
tabBarHeight = 49.0 + environment.safeInsets.bottom
|
||||
}
|
||||
let containerInsets = UIEdgeInsets(
|
||||
top: environment.safeInsets.top,
|
||||
left: environment.safeInsets.left + 12.0,
|
||||
bottom: tabBarHeight + 3.0,
|
||||
right: environment.safeInsets.right + 12.0
|
||||
)
|
||||
|
||||
let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom)
|
||||
|
||||
let spacing: CGFloat = 8.0
|
||||
|
||||
let iconSize = CGSize(width: 30.0, height: 30.0)
|
||||
|
||||
let tooltipText: String
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
var animationName: String = "anim_forward"
|
||||
if component.peer.id == component.context.account.peerId {
|
||||
tooltipText = environment.strings.Conversation_ForwardTooltip_SavedMessages_One
|
||||
overrideImage = .savedMessagesIcon
|
||||
animationName = "anim_savedmessages"
|
||||
} else {
|
||||
tooltipText = environment.strings.Conversation_ForwardTooltip_Chat_One(component.peer.compactDisplayTitle).string
|
||||
}
|
||||
|
||||
let contentSize = self.content.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(text: .markdown(
|
||||
text: tooltipText,
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)),
|
||||
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
|
||||
linkAttribute: { _ in return nil })
|
||||
))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - 16.0, height: availableContentSize.height)
|
||||
)
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height)
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
|
||||
self.avatarNode.setPeer(context: component.context, theme: environment.theme, peer: component.peer, overrideImage: overrideImage, synchronousLoad: true)
|
||||
self.avatarNode.updateSize(size: avatarFrame.size)
|
||||
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
|
||||
|
||||
let _ = self.animation.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: animationName
|
||||
),
|
||||
size: CGSize(width: 38.0, height: 38.0),
|
||||
loop: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: iconSize
|
||||
)
|
||||
if let animationView = self.animation.view {
|
||||
if animationView.superview == nil {
|
||||
animationView.alpha = 0.0
|
||||
self.avatarNode.view.addSubview(animationView)
|
||||
}
|
||||
animationView.frame = CGRect(origin: .zero, size: iconSize).insetBy(dx: -2.0, dy: -2.0)
|
||||
}
|
||||
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.contentView.addSubview(contentView)
|
||||
}
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize))
|
||||
}
|
||||
|
||||
let size = CGSize(width: availableContentSize.width, height: contentHeight)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size)
|
||||
|
||||
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: backgroundFrame.size))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class QuickShareToastScreen: ViewControllerComponentContainer {
|
||||
private var processedDidAppear: Bool = false
|
||||
private var processedDidDisappear: Bool = false
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peer: EnginePeer,
|
||||
sourceFrame: CGRect,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
super.init(
|
||||
context: context,
|
||||
component: QuickShareToastScreenComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
sourceFrame: sourceFrame,
|
||||
action: action
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
presentationMode: .default,
|
||||
updatedPresentationData: nil
|
||||
)
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.processedDidAppear {
|
||||
self.processedDidAppear = true
|
||||
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
|
||||
componentView.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func superDismiss() {
|
||||
super.dismiss()
|
||||
}
|
||||
|
||||
override func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.processedDidDisappear {
|
||||
self.processedDidDisappear = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
|
||||
componentView.animateOut(completion: { [weak self] in
|
||||
if let self {
|
||||
self.superDismiss()
|
||||
}
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
super.dismiss(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -280,7 +280,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public let attemptedNavigationToPrivateQuote: (Peer?) -> Void
|
||||
public let forceUpdateWarpContents: () -> Void
|
||||
public let playShakeAnimation: () -> Void
|
||||
public let displayQuickShare: (ASDisplayNode, ContextGesture) -> Void
|
||||
public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> Void
|
||||
|
||||
public var canPlayMedia: Bool = false
|
||||
public var hiddenMedia: [MessageId: [Media]] = [:]
|
||||
@ -440,7 +440,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void,
|
||||
forceUpdateWarpContents: @escaping () -> Void,
|
||||
playShakeAnimation: @escaping () -> Void,
|
||||
displayQuickShare: @escaping (ASDisplayNode, ContextGesture) -> Void,
|
||||
displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void,
|
||||
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState,
|
||||
stickerSettings: ChatInterfaceStickerSettings,
|
||||
|
@ -746,6 +746,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
private var storyPeerList: ComponentView<Empty>?
|
||||
public var storyPeerAction: ((EnginePeer?) -> Void)?
|
||||
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
|
||||
public var storyComposeAction: ((CGFloat) -> Void)?
|
||||
|
||||
private var effectiveContentView: ContentView? {
|
||||
return self.secondaryContentView ?? self.primaryContentView
|
||||
@ -977,6 +978,12 @@ public final class ChatListHeaderComponent: Component {
|
||||
return
|
||||
}
|
||||
self.component?.toggleIsLocked()
|
||||
},
|
||||
composeAction: { [weak self] offset in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.storyComposeAction?(offset)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -130,8 +130,6 @@ final class GiftOptionsScreenComponent: Component {
|
||||
private let premiumTitle = ComponentView<Empty>()
|
||||
private let premiumDescription = ComponentView<Empty>()
|
||||
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var inProgressPremiumGift: String?
|
||||
private let purchaseDisposable = MetaDisposable()
|
||||
|
||||
private let starsTitle = ComponentView<Empty>()
|
||||
private let starsDescription = ComponentView<Empty>()
|
||||
@ -251,7 +249,6 @@ final class GiftOptionsScreenComponent: Component {
|
||||
|
||||
deinit {
|
||||
self.starsStateDisposable?.dispose()
|
||||
self.purchaseDisposable.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -262,21 +259,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate)
|
||||
}
|
||||
|
||||
private func dismissAllTooltips(controller: ViewController) {
|
||||
controller.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitAction()
|
||||
}
|
||||
return true
|
||||
})
|
||||
controller.window?.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitAction()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
|
||||
guard let environment = self.environment, let component = self.component else {
|
||||
return
|
||||
@ -725,12 +708,8 @@ final class GiftOptionsScreenComponent: Component {
|
||||
|
||||
let bottomContentInset: CGFloat = 24.0
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let sectionSpacing: CGFloat = 24.0
|
||||
let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left
|
||||
|
||||
let _ = bottomContentInset
|
||||
let _ = sectionSpacing
|
||||
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled || state.disallowedGifts?.contains(.premium) == true
|
||||
@ -1056,7 +1035,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
color: .purple
|
||||
)
|
||||
},
|
||||
isLoading: self.inProgressPremiumGift == product.id
|
||||
isLoading: false
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
|
@ -3744,7 +3744,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _ ,_ in
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in
|
||||
@ -10068,7 +10068,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
cameraTransitionIn = StoryCameraTransitionIn(
|
||||
sourceView: rightButton.view,
|
||||
sourceRect: rightButton.view.bounds,
|
||||
sourceCornerRadius: rightButton.view.bounds.height * 0.5
|
||||
sourceCornerRadius: rightButton.view.bounds.height * 0.5,
|
||||
useFillAnimation: false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,44 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
load(
|
||||
"@build_bazel_rules_apple//apple:resources.bzl",
|
||||
"apple_resource_bundle",
|
||||
"apple_resource_group",
|
||||
)
|
||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
"plist_fragment",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "StoryPeerListMetalResources",
|
||||
srcs = glob([
|
||||
"MetalResources/**/*.*",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "StoryPeerListBundleInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.telegram.StoryPeerList</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>StoryPeerList</string>
|
||||
"""
|
||||
)
|
||||
|
||||
apple_resource_bundle(
|
||||
name = "StoryPeerListBundle",
|
||||
infoplists = [
|
||||
":StoryPeerListBundleInfoPlist",
|
||||
],
|
||||
resources = [
|
||||
":StoryPeerListMetalResources",
|
||||
],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "StoryPeerListComponent",
|
||||
@ -8,9 +48,13 @@ swift_library(
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
data = [
|
||||
":StoryPeerListBundle",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/MetalEngine",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
|
@ -0,0 +1,109 @@
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct QuadVertexOut {
|
||||
float4 position [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
constant static float2 quadVertices[6] = {
|
||||
float2(0.0, 0.0),
|
||||
float2(1.0, 0.0),
|
||||
float2(0.0, 1.0),
|
||||
float2(1.0, 0.0),
|
||||
float2(0.0, 1.0),
|
||||
float2(1.0, 1.0)
|
||||
};
|
||||
|
||||
vertex QuadVertexOut cameraBlobVertex(
|
||||
constant float4 &rect [[ buffer(0) ]],
|
||||
uint vid [[ vertex_id ]]
|
||||
) {
|
||||
float2 quadVertex = quadVertices[vid];
|
||||
|
||||
QuadVertexOut out;
|
||||
out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0);
|
||||
out.position.x = -1.0 + out.position.x * 2.0;
|
||||
out.position.y = -1.0 + out.position.y * 2.0;
|
||||
|
||||
out.uv = quadVertex;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
#define BindingDistance 0.45
|
||||
#define AARadius 2.0
|
||||
#define PersistenceFactor 1.1
|
||||
|
||||
float smin(float a, float b, float k) {
|
||||
float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
|
||||
h = pow(h, 0.6);
|
||||
return mix(a, b, h) - k * h * (1.0 - h) * 1.0;
|
||||
}
|
||||
|
||||
float sdfCircle(float2 uv, float2 position, float radius) {
|
||||
return length(uv - position) - radius;
|
||||
}
|
||||
|
||||
float dynamicBinding(float2 primaryPos, float2 secondaryPos) {
|
||||
float distance = length(primaryPos - secondaryPos);
|
||||
return BindingDistance * (1.0 + PersistenceFactor * smoothstep(0.0, 2.0, distance));
|
||||
}
|
||||
|
||||
float map(float2 uv, float2 primaryParameters, float2 primaryOffset, float2 secondaryParameters, float2 secondaryOffset) {
|
||||
float primary = sdfCircle(uv, primaryOffset, primaryParameters.x);
|
||||
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
|
||||
|
||||
float bindDist = dynamicBinding(primaryOffset, secondaryOffset);
|
||||
|
||||
float metaballs = 1.0;
|
||||
metaballs = smin(metaballs, primary, bindDist);
|
||||
metaballs = smin(metaballs, secondary, bindDist);
|
||||
return metaballs;
|
||||
}
|
||||
|
||||
fragment half4 cameraBlobFragment(
|
||||
QuadVertexOut in [[stage_in]],
|
||||
constant float2 &primaryParameters [[buffer(0)]],
|
||||
constant float2 &primaryOffset [[buffer(1)]],
|
||||
constant float2 &secondaryParameters [[buffer(2)]],
|
||||
constant float2 &secondaryOffset [[buffer(3)]],
|
||||
constant float2 &resolution [[buffer(4)]]
|
||||
) {
|
||||
float aspectRatio = resolution.x / resolution.y;
|
||||
float2 uv = in.uv * 2.0 - 1.0;
|
||||
uv.x *= aspectRatio;
|
||||
|
||||
float t = AARadius / resolution.y;
|
||||
|
||||
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
|
||||
|
||||
if (primaryParameters.y > 0) {
|
||||
float innerHoleRadius = primaryParameters.y;
|
||||
float hole = smoothstep(-t, t, length(uv - primaryOffset) - innerHoleRadius);
|
||||
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
|
||||
|
||||
c *= mix(1.0, hole, primaryInfluence);
|
||||
} else if (primaryParameters.y < 0) {
|
||||
float cutRadius = abs(primaryParameters.y);
|
||||
float2 primaryFeatheredOffset = primaryOffset;
|
||||
primaryFeatheredOffset.x *= 1.129;
|
||||
|
||||
float distFromCenter = length(uv - primaryFeatheredOffset);
|
||||
|
||||
float gradientWidth = 0.21;
|
||||
float featheredEdge = smoothstep(cutRadius - gradientWidth, cutRadius + gradientWidth, distFromCenter);
|
||||
|
||||
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
|
||||
|
||||
float horizontalFade = 1.0;
|
||||
float rightEdgePosition = 0.94 * aspectRatio;
|
||||
if (uv.x > rightEdgePosition) {
|
||||
horizontalFade = 1.0 - smoothstep(0.0, 0.22, uv.x - rightEdgePosition);
|
||||
}
|
||||
|
||||
c *= mix(1.0, featheredEdge, primaryInfluence) * horizontalFade;
|
||||
}
|
||||
|
||||
return half4(0.0, 0.0, 0.0, c);
|
||||
}
|
@ -0,0 +1,545 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import Metal
|
||||
import MetalKit
|
||||
import MetalEngine
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
|
||||
private var metalLibraryValue: MTLLibrary?
|
||||
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
|
||||
if let metalLibraryValue {
|
||||
return metalLibraryValue
|
||||
}
|
||||
|
||||
let mainBundle = Bundle(for: StoryBlobLayer.self)
|
||||
guard let path = mainBundle.path(forResource: "StoryPeerListBundle", ofType: "bundle") else {
|
||||
return nil
|
||||
}
|
||||
guard let bundle = Bundle(path: path) else {
|
||||
return nil
|
||||
}
|
||||
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
metalLibraryValue = library
|
||||
return library
|
||||
}
|
||||
|
||||
private final class PropertyAnimation<T: Interpolatable> {
|
||||
let from: T
|
||||
let to: T
|
||||
let animation: ComponentTransition.Animation
|
||||
let startTimestamp: Double
|
||||
private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable
|
||||
|
||||
init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) {
|
||||
self.from = fromValue
|
||||
self.to = toValue
|
||||
self.animation = animation
|
||||
self.startTimestamp = startTimestamp
|
||||
self.interpolator = T.interpolator()
|
||||
}
|
||||
|
||||
func valueAt(_ t: CGFloat) -> Interpolatable {
|
||||
if t <= 0.0 {
|
||||
return self.from
|
||||
} else if t >= 1.0 {
|
||||
return self.to
|
||||
} else {
|
||||
return self.interpolator(self.from, self.to, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatableProperty<T: Interpolatable> {
|
||||
var presentationValue: T
|
||||
var value: T
|
||||
private var animation: PropertyAnimation<T>?
|
||||
|
||||
init(value: T) {
|
||||
self.value = value
|
||||
self.presentationValue = value
|
||||
}
|
||||
|
||||
func update(value: T, transition: ComponentTransition = .immediate) {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if case .none = transition.animation {
|
||||
if let animation = self.animation, case let .curve(duration, curve) = animation.animation {
|
||||
self.value = value
|
||||
let elapsed = duration - (currentTimestamp - animation.startTimestamp)
|
||||
if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 {
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp)
|
||||
} else {
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp)
|
||||
}
|
||||
} else {
|
||||
self.value = value
|
||||
self.presentationValue = value
|
||||
self.animation = nil
|
||||
}
|
||||
} else {
|
||||
self.value = value
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func tick(timestamp: Double) -> Bool {
|
||||
guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else {
|
||||
return false
|
||||
}
|
||||
|
||||
let timeFromStart = timestamp - animation.startTimestamp
|
||||
var t = max(0.0, timeFromStart / duration)
|
||||
switch curve {
|
||||
case .linear:
|
||||
break
|
||||
case .easeInOut:
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
case .spring:
|
||||
t = lookupSpringValue(t)
|
||||
case let .custom(x1, y1, x2, y2):
|
||||
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
|
||||
}
|
||||
self.presentationValue = animation.valueAt(t) as! T
|
||||
|
||||
if timeFromStart <= duration {
|
||||
return true
|
||||
}
|
||||
self.animation = nil
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
|
||||
let table: [(CGFloat, CGFloat)] = [
|
||||
(0.0, 0.0),
|
||||
(0.0625, 0.1123005598783493),
|
||||
(0.125, 0.31598418951034546),
|
||||
(0.1875, 0.5103585720062256),
|
||||
(0.25, 0.6650152802467346),
|
||||
(0.3125, 0.777747631072998),
|
||||
(0.375, 0.8557760119438171),
|
||||
(0.4375, 0.9079672694206238),
|
||||
(0.5, 0.942038357257843),
|
||||
(0.5625, 0.9638798832893372),
|
||||
(0.625, 0.9776856303215027),
|
||||
(0.6875, 0.9863143563270569),
|
||||
(0.75, 0.991658091545105),
|
||||
(0.8125, 0.9949421286582947),
|
||||
(0.875, 0.9969474077224731),
|
||||
(0.9375, 0.9981651306152344),
|
||||
(1.0, 1.0)
|
||||
]
|
||||
|
||||
for i in 0 ..< table.count - 2 {
|
||||
let lhs = table[i]
|
||||
let rhs = table[i + 1]
|
||||
|
||||
if t >= lhs.0 && t <= rhs.0 {
|
||||
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
|
||||
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
final class StoryBlobLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
var internalData: MetalEngineSubjectInternalData?
|
||||
|
||||
private final class RenderState: RenderToLayerState {
|
||||
let pipelineState: MTLRenderPipelineState
|
||||
|
||||
required init?(device: MTLDevice) {
|
||||
guard let library = metalLibrary(device: device) else {
|
||||
return nil
|
||||
}
|
||||
guard let vertexFunction = library.makeFunction(name: "cameraBlobVertex"),
|
||||
let fragmentFunction = library.makeFunction(name: "cameraBlobFragment") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
pipelineDescriptor.vertexFunction = vertexFunction
|
||||
pipelineDescriptor.fragmentFunction = fragmentFunction
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
|
||||
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
|
||||
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
|
||||
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
|
||||
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
|
||||
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
|
||||
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
self.pipelineState = pipelineState
|
||||
}
|
||||
}
|
||||
|
||||
private var primarySize = AnimatableProperty<CGFloat>(value: 1.0)
|
||||
private var primaryHoleSize = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
|
||||
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
|
||||
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.85)
|
||||
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
|
||||
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
|
||||
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
|
||||
private var hasActiveAnimations: Bool = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isOpaque = false
|
||||
|
||||
self.didEnterHierarchy = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateAnimations()
|
||||
if self.hasActiveAnimations {
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.didExitHierarchy = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.displayLinkSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
|
||||
if let layer = layer as? StoryBlobLayer {
|
||||
self.primarySize = layer.primarySize
|
||||
self.primaryHoleSize = layer.primaryHoleSize
|
||||
self.primaryOffsetX = layer.primaryOffsetX
|
||||
self.primaryOffsetY = layer.primaryOffsetY
|
||||
self.secondarySize = layer.secondarySize
|
||||
self.secondaryOffsetX = layer.secondaryOffsetX
|
||||
self.secondaryOffsetY = layer.secondaryOffsetY
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updatePrimarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedSize = size / self.bounds.height
|
||||
self.primarySize.update(value: mappedSize, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func updatePrimaryHoleSize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedSize = size / self.bounds.height
|
||||
self.primaryHoleSize.update(value: mappedSize, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.bounds.height * 2.0
|
||||
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.width > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.bounds.width * 2.0
|
||||
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func updateSecondarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedSize = size / self.bounds.height
|
||||
self.secondarySize.update(value: mappedSize, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.bounds.height * 2.0
|
||||
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||
guard self.bounds.width > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.bounds.width * 2.0
|
||||
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.setNeedsUpdate()
|
||||
}
|
||||
|
||||
private func updateAnimations() {
|
||||
let properties = [
|
||||
self.primarySize,
|
||||
self.primaryHoleSize,
|
||||
self.primaryOffsetX,
|
||||
self.primaryOffsetY,
|
||||
self.secondarySize,
|
||||
self.secondaryOffsetX,
|
||||
self.secondaryOffsetY,
|
||||
]
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
var hasAnimations = false
|
||||
for property in properties {
|
||||
if property.tick(timestamp: timestamp) {
|
||||
hasAnimations = true
|
||||
}
|
||||
}
|
||||
self.hasActiveAnimations = hasAnimations
|
||||
}
|
||||
|
||||
func update(context: MetalEngineSubjectContext) {
|
||||
if self.bounds.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
|
||||
|
||||
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, commands: { encoder, placement in
|
||||
let effectiveRect = placement.effectiveRect
|
||||
|
||||
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
|
||||
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
||||
|
||||
var primaryParameters = simd_float2(
|
||||
Float(self.primarySize.presentationValue),
|
||||
Float(self.primaryHoleSize.presentationValue)
|
||||
)
|
||||
encoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float2>.size, index: 0)
|
||||
|
||||
var primaryOffset = simd_float2(
|
||||
Float(self.primaryOffsetX.presentationValue),
|
||||
Float(self.primaryOffsetY.presentationValue)
|
||||
)
|
||||
encoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 1)
|
||||
|
||||
var secondaryParameters = simd_float2(
|
||||
Float(self.secondarySize.presentationValue),
|
||||
Float(0)
|
||||
)
|
||||
encoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float2>.size, index: 2)
|
||||
|
||||
var secondaryOffset = simd_float2(
|
||||
Float(self.secondaryOffsetX.presentationValue),
|
||||
Float(self.secondaryOffsetY.presentationValue)
|
||||
)
|
||||
encoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 3)
|
||||
|
||||
var resolution = simd_float2(
|
||||
Float(drawableSize.width),
|
||||
Float(drawableSize.height)
|
||||
)
|
||||
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_float2>.size, index: 4)
|
||||
|
||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
final class StoryComposeLayer: CALayer {
|
||||
private let theme: PresentationTheme?
|
||||
private let strings: PresentationStrings?
|
||||
|
||||
private let blobLayer = SimpleLayer()
|
||||
private let maskLayer = StoryBlobLayer()
|
||||
private let backgroundLayer = SimpleGradientLayer()
|
||||
private let foregroundLayer = SimpleGradientLayer()
|
||||
private let iconLayer = SimpleLayer()
|
||||
private let labelLayer = SimpleLayer()
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSublayer(self.blobLayer)
|
||||
|
||||
self.blobLayer.mask = self.maskLayer
|
||||
self.blobLayer.masksToBounds = true
|
||||
|
||||
self.backgroundLayer.type = .axial
|
||||
self.backgroundLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
self.backgroundLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
|
||||
self.blobLayer.addSublayer(self.backgroundLayer)
|
||||
|
||||
self.foregroundLayer.type = .axial
|
||||
self.foregroundLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
self.foregroundLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
|
||||
self.blobLayer.addSublayer(self.foregroundLayer)
|
||||
|
||||
self.iconLayer.contents = generateAddIcon(color: theme.list.itemCheckColors.foregroundColor)?.cgImage
|
||||
self.iconLayer.opacity = 0.0
|
||||
self.blobLayer.addSublayer(self.iconLayer)
|
||||
|
||||
if let image = generateAddLabel(strings: strings, color: theme.list.itemPrimaryTextColor) {
|
||||
self.labelLayer.contents = image.cgImage
|
||||
self.labelLayer.bounds = CGRect(origin: .zero, size: image.size)
|
||||
self.addSublayer(self.labelLayer)
|
||||
self.labelLayer.opacity = 0.0
|
||||
}
|
||||
|
||||
if let blurEffect = CALayer.blur() {
|
||||
self.iconLayer.filters = [blurEffect]
|
||||
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
|
||||
self.labelLayer.filters = [blurEffect]
|
||||
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
|
||||
}
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
if let layer = layer as? StoryComposeLayer {
|
||||
self.theme = layer.theme
|
||||
self.strings = layer.strings
|
||||
} else {
|
||||
self.theme = nil
|
||||
self.strings = nil
|
||||
}
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateOffset(_ offset: CGFloat, baseSize: CGFloat, colors: [CGColor], transition: ComponentTransition = .immediate) {
|
||||
self.backgroundLayer.colors = colors
|
||||
self.foregroundLayer.locations = [0.0, 0.68, 1.0]
|
||||
|
||||
if let theme = self.theme {
|
||||
let fillColor = theme.list.itemCheckColors.fillColor
|
||||
self.foregroundLayer.colors = [
|
||||
fillColor.cgColor,
|
||||
fillColor.withAlphaComponent(0.0).cgColor,
|
||||
fillColor.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
}
|
||||
|
||||
var holeSize = baseSize > 52.0 ? 58.0 : -60.0
|
||||
var primaryBaseScale = 1.0
|
||||
var secondaryBaseScale = 0.68
|
||||
if holeSize < 0.0 {
|
||||
if offset > 48.0 {
|
||||
holeSize -= min(14.0, (max(0.0, (offset - 48.0)) / 20.0) * 14.0)
|
||||
}
|
||||
primaryBaseScale = 0.96 + min(1.0, max(0.0, (offset - 20.0)) / 10.0) * 0.04
|
||||
secondaryBaseScale = 0.62
|
||||
}
|
||||
|
||||
var secondaryScale = secondaryBaseScale
|
||||
if offset < 20.0 {
|
||||
secondaryScale += (offset / 20.0) * 0.2
|
||||
} else {
|
||||
secondaryScale = min(1.0, max(secondaryBaseScale + 0.2, offset / 75.0))
|
||||
}
|
||||
|
||||
self.maskLayer.updatePrimaryOffsetX(85.0, transition: transition)
|
||||
self.maskLayer.updatePrimarySize(baseSize * primaryBaseScale, transition: transition)
|
||||
self.maskLayer.updateSecondaryOffsetX(85.0 - offset, transition: transition)
|
||||
self.maskLayer.updatePrimaryHoleSize(holeSize, transition: transition)
|
||||
self.maskLayer.updateSecondarySize(55.0 * secondaryScale, transition: transition)
|
||||
|
||||
if holeSize < 0.0 {
|
||||
transition.setAlpha(layer: self.blobLayer, alpha: min(1.0, max(0.0, offset / 10.0)))
|
||||
}
|
||||
|
||||
let layerX = self.bounds.width - offset + 5.0
|
||||
transition.setPosition(layer: self.iconLayer, position: CGPoint(x: layerX, y: self.bounds.height / 2.0))
|
||||
transition.setPosition(layer: self.labelLayer, position: CGPoint(x: layerX, y: self.bounds.height + 13.0 - UIScreenPixel))
|
||||
|
||||
let iconOffset = max(0.0, offset - 35.0)
|
||||
let alpha = max(0.0, min(1.0, iconOffset / 15.0))
|
||||
let blurRadius = 8.0 - min(8.0, (iconOffset / 20.0) * 8.0)
|
||||
|
||||
for layer in [self.iconLayer, self.labelLayer] {
|
||||
layer.setValue(blurRadius as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius")
|
||||
transition.setAlpha(layer: layer, alpha: alpha)
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSublayers() {
|
||||
super.layoutSublayers()
|
||||
|
||||
self.blobLayer.frame = self.bounds
|
||||
self.maskLayer.frame = self.bounds
|
||||
self.backgroundLayer.frame = self.bounds
|
||||
self.foregroundLayer.frame = self.bounds
|
||||
self.iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
|
||||
}
|
||||
}
|
||||
|
||||
private func generateAddIcon(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(3.0)
|
||||
context.setLineCap(.round)
|
||||
|
||||
context.move(to: CGPoint(x: 15.0, y: 5.5))
|
||||
context.addLine(to: CGPoint(x: 15.0, y: 24.5))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 5.5, y: 15.0))
|
||||
context.addLine(to: CGPoint(x: 24.5, y: 15.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? {
|
||||
let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
|
||||
var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
textRect.size.width = ceil(textRect.size.width)
|
||||
textRect.size.height = ceil(textRect.size.height)
|
||||
|
||||
return generateImage(textRect.size, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
titleString.draw(in: textRect)
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
@ -60,6 +60,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
public let openStatusSetup: (UIView) -> Void
|
||||
public let lockAction: () -> Void
|
||||
public let composeAction: (CGFloat) -> Void
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@ -81,7 +82,8 @@ public final class StoryPeerListComponent: Component {
|
||||
peerAction: @escaping (EnginePeer?) -> Void,
|
||||
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void,
|
||||
openStatusSetup: @escaping (UIView) -> Void,
|
||||
lockAction: @escaping () -> Void
|
||||
lockAction: @escaping () -> Void,
|
||||
composeAction: @escaping (CGFloat) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -103,6 +105,7 @@ public final class StoryPeerListComponent: Component {
|
||||
self.contextPeerAction = contextPeerAction
|
||||
self.openStatusSetup = openStatusSetup
|
||||
self.lockAction = lockAction
|
||||
self.composeAction = composeAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
||||
@ -162,7 +165,6 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
private final class VisibleItem {
|
||||
let view = ComponentView<Empty>()
|
||||
var hasBlur: Bool = false
|
||||
|
||||
init() {
|
||||
}
|
||||
@ -360,6 +362,8 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
private var sharedBlurEffect: NSObject?
|
||||
|
||||
private var willComposeOnRelease = false
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.collapsedButton = HighlightableButton()
|
||||
|
||||
@ -561,14 +565,62 @@ public final class StoryPeerListComponent: Component {
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
|
||||
let willComposeOnRelease = scrollView.contentOffset.x <= -70.0
|
||||
if self.willComposeOnRelease != willComposeOnRelease {
|
||||
self.willComposeOnRelease = willComposeOnRelease
|
||||
|
||||
if willComposeOnRelease {
|
||||
HapticFeedback().tap()
|
||||
} else {
|
||||
HapticFeedback().impact(.veryLight)
|
||||
}
|
||||
}
|
||||
|
||||
if scrollView.isScrollEnabled && scrollView.isTracking, scrollView.contentOffset.x <= -85.0 {
|
||||
scrollView.isScrollEnabled = false
|
||||
scrollView.panGestureRecognizer.isEnabled = false
|
||||
scrollView.panGestureRecognizer.isEnabled = true
|
||||
scrollView.contentOffset = CGPoint(x: -85.0, y: 0.0)
|
||||
|
||||
self.willComposeOnRelease = false
|
||||
Queue.mainQueue().after(0.5) {
|
||||
scrollView.isScrollEnabled = true
|
||||
scrollView.contentOffset = .zero
|
||||
}
|
||||
if let component = self.component {
|
||||
HapticFeedback().tap()
|
||||
component.composeAction(abs(scrollView.contentOffset.x))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !self.ignoreScrolling {
|
||||
if scrollView.isScrollEnabled && scrollView.contentOffset.x <= -70.0 {
|
||||
scrollView.isScrollEnabled = false
|
||||
scrollView.panGestureRecognizer.isEnabled = false
|
||||
scrollView.panGestureRecognizer.isEnabled = true
|
||||
scrollView.contentOffset = CGPoint(x: max(-85.0, scrollView.contentOffset.x), y: 0.0)
|
||||
|
||||
self.willComposeOnRelease = false
|
||||
Queue.mainQueue().after(0.5) {
|
||||
scrollView.isScrollEnabled = true
|
||||
scrollView.contentOffset = .zero
|
||||
}
|
||||
if let component = self.component {
|
||||
HapticFeedback().tap()
|
||||
component.composeAction(abs(scrollView.contentOffset.x))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let titleIconSpacing: CGFloat = 4.0
|
||||
let titleIndicatorSpacing: CGFloat = 8.0
|
||||
|
||||
@ -1072,6 +1124,11 @@ public final class StoryPeerListComponent: Component {
|
||||
totalCount = itemSet.storyCount
|
||||
unseenCount = itemSet.unseenCount
|
||||
|
||||
var composeContentOffset: CGFloat?
|
||||
if peer.id == component.context.account.peerId && collapsedState.sideAlphaFraction == 1.0 && self.scrollView.contentOffset.x < 0.0 {
|
||||
composeContentOffset = self.scrollView.contentOffset.x * -1.0
|
||||
}
|
||||
|
||||
let _ = visibleItem.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(StoryPeerListItemComponent(
|
||||
@ -1090,6 +1147,7 @@ public final class StoryPeerListComponent: Component {
|
||||
expandEffectFraction: collapsedState.expandEffectFraction,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
rightNeighborDistance: rightNeighborDistance,
|
||||
composeContentOffset: composeContentOffset,
|
||||
action: component.peerAction,
|
||||
contextGesture: component.contextPeerAction
|
||||
)),
|
||||
@ -1228,6 +1286,7 @@ public final class StoryPeerListComponent: Component {
|
||||
expandEffectFraction: collapsedState.expandEffectFraction,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
rightNeighborDistance: rightNeighborDistance,
|
||||
composeContentOffset: nil,
|
||||
action: component.peerAction,
|
||||
contextGesture: component.contextPeerAction
|
||||
)),
|
||||
|
@ -380,6 +380,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
public let expandEffectFraction: CGFloat
|
||||
public let leftNeighborDistance: CGPoint?
|
||||
public let rightNeighborDistance: CGPoint?
|
||||
public let composeContentOffset: CGFloat?
|
||||
public let action: (EnginePeer) -> Void
|
||||
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
|
||||
@ -399,6 +400,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
expandEffectFraction: CGFloat,
|
||||
leftNeighborDistance: CGPoint?,
|
||||
rightNeighborDistance: CGPoint?,
|
||||
composeContentOffset: CGFloat?,
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
) {
|
||||
@ -417,6 +419,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.expandEffectFraction = expandEffectFraction
|
||||
self.leftNeighborDistance = leftNeighborDistance
|
||||
self.rightNeighborDistance = rightNeighborDistance
|
||||
self.composeContentOffset = composeContentOffset
|
||||
self.action = action
|
||||
self.contextGesture = contextGesture
|
||||
}
|
||||
@ -467,6 +470,9 @@ public final class StoryPeerListItemComponent: Component {
|
||||
if lhs.rightNeighborDistance != rhs.rightNeighborDistance {
|
||||
return false
|
||||
}
|
||||
if lhs.composeContentOffset != rhs.composeContentOffset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -479,6 +485,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
|
||||
fileprivate var composeLayer: StoryComposeLayer?
|
||||
fileprivate let avatarContent: PortalSourceView
|
||||
private let avatarContainer: UIView
|
||||
private let avatarBackgroundContainer: UIView
|
||||
@ -494,12 +501,11 @@ public final class StoryPeerListItemComponent: Component {
|
||||
private let indicatorShapeSeenLayer: SimpleShapeLayer
|
||||
private let indicatorShapeUnseenLayer: SimpleShapeLayer
|
||||
private let title = ComponentView<Empty>()
|
||||
private let composeTitle = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryPeerListItemComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
private var demoLoading = false
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.backgroundContainer = UIView()
|
||||
self.backgroundContainer.isUserInteractionEnabled = false
|
||||
@ -960,6 +966,32 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height))
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
var baseSize: CGFloat = 60.0
|
||||
var effectiveColors = component.unseenCount > 0 ? unseenColors : seenColors
|
||||
if self.avatarAddBadgeView != nil {
|
||||
baseSize = 52.0
|
||||
effectiveColors = [component.theme.list.itemCheckColors.fillColor.cgColor, component.theme.list.itemCheckColors.fillColor.cgColor]
|
||||
}
|
||||
if let composeContentOffset = component.composeContentOffset {
|
||||
let composeLayer: StoryComposeLayer
|
||||
if let current = self.composeLayer {
|
||||
composeLayer = current
|
||||
} else {
|
||||
composeLayer = StoryComposeLayer(theme: component.theme, strings: component.strings)
|
||||
self.composeLayer = composeLayer
|
||||
self.layer.addSublayer(composeLayer)
|
||||
}
|
||||
|
||||
composeLayer.frame = CGRect(origin: CGPoint(x: size.width - 195.0, y: 0.0), size: CGSize(width: 160.0, height: 60.0))
|
||||
composeLayer.updateOffset(composeContentOffset, baseSize: baseSize, colors: effectiveColors, transition: transition)
|
||||
} else if let composeLayer = self.composeLayer {
|
||||
self.composeLayer = nil
|
||||
composeLayer.updateOffset(0.0, baseSize: baseSize, colors: effectiveColors, transition: .easeInOut(duration: 0.2))
|
||||
Queue.mainQueue().after(0.21, {
|
||||
composeLayer.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
@ -418,7 +418,7 @@ extension ChatControllerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.dismissPreviewing?()
|
||||
let _ = strongSelf.dismissPreviewing?(false)
|
||||
}
|
||||
}))
|
||||
case .replyThread:
|
||||
|
@ -292,11 +292,20 @@ extension ChatControllerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
var keepDefaultContentTouches = false
|
||||
for media in message.media {
|
||||
if media is TelegramMediaImage {
|
||||
keepDefaultContentTouches = true
|
||||
} else if let file = media as? TelegramMediaFile, file.isVideo {
|
||||
keepDefaultContentTouches = true
|
||||
}
|
||||
}
|
||||
|
||||
let source: ContextContentSource
|
||||
if let location = location {
|
||||
source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
|
||||
} else {
|
||||
source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll))
|
||||
source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll, keepDefaultContentTouches: keepDefaultContentTouches))
|
||||
}
|
||||
|
||||
self.canReadHistory.set(false)
|
||||
|
@ -1,15 +1,52 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import ContextUI
|
||||
import QuickShareScreen
|
||||
|
||||
extension ChatControllerImpl {
|
||||
func displayQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
|
||||
guard !"".isEmpty else {
|
||||
return
|
||||
}
|
||||
let controller = QuickShareScreen(context: self.context, sourceNode: node, gesture: gesture)
|
||||
func displayQuickShare(id: EngineMessage.Id, node: ASDisplayNode, gesture: ContextGesture) {
|
||||
let controller = QuickShareScreen(
|
||||
context: self.context,
|
||||
sourceNode: node,
|
||||
gesture: gesture,
|
||||
openPeer: { [weak self] peerId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
|
||||
})
|
||||
},
|
||||
completion: { [weak self] peerId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let enqueueMessage = StandaloneSendEnqueueMessage(
|
||||
content: .forward(forward: StandaloneSendEnqueueMessage.Forward(
|
||||
sourceId: id,
|
||||
threadId: nil
|
||||
)),
|
||||
replyToMessageId: nil
|
||||
)
|
||||
let _ = (standaloneSendEnqueueMessages(
|
||||
accountPeerId: self.context.account.peerId,
|
||||
postbox: self.context.account.postbox,
|
||||
network: self.context.account.network,
|
||||
stateManager: self.context.account.stateManager,
|
||||
auxiliaryMethods: self.context.account.auxiliaryMethods,
|
||||
peerId: peerId,
|
||||
threadId: nil,
|
||||
messages: [enqueueMessage]
|
||||
)).startStandalone()
|
||||
}
|
||||
)
|
||||
self.presentInGlobalOverlay(controller)
|
||||
}
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var botStart: ChatControllerInitialBotStart?
|
||||
var attachBotStart: ChatControllerInitialAttachBotStart?
|
||||
var botAppStart: ChatControllerInitialBotAppStart?
|
||||
let mode: ChatControllerPresentationMode
|
||||
var mode: ChatControllerPresentationMode
|
||||
|
||||
let peerDisposable = MetaDisposable()
|
||||
let titleDisposable = MetaDisposable()
|
||||
@ -575,7 +575,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)?
|
||||
|
||||
public var purposefulAction: (() -> Void)?
|
||||
public var dismissPreviewing: (() -> Void)?
|
||||
public var dismissPreviewing: ((Bool) -> (() -> Void))?
|
||||
|
||||
var updatedClosedPinnedMessageId: ((MessageId) -> Void)?
|
||||
var requestedUnpinAllMessages: ((Int, MessageId) -> Void)?
|
||||
@ -893,6 +893,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return false
|
||||
}
|
||||
|
||||
if let contextController = self.currentContextController {
|
||||
self.present(contextController, in: .window(.root))
|
||||
Queue.mainQueue().after(0.15) {
|
||||
contextController.dismiss(result: .dismissWithoutContent, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
let mode = params.mode
|
||||
|
||||
let displayVoiceMessageDiscardAlert: () -> Bool = { [weak self] in
|
||||
@ -1360,7 +1367,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view)
|
||||
if let contextController = self.currentContextController {
|
||||
contextController.view.addSubview(view)
|
||||
} else {
|
||||
self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view)
|
||||
}
|
||||
}, openUrl: { [weak self] url in
|
||||
self?.openUrl(url, concealed: false, skipConcealedAlert: isLocation, message: nil)
|
||||
}, openPeer: { [weak self] peer, navigation in
|
||||
@ -4850,11 +4861,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
self.playShakeAnimation()
|
||||
}, displayQuickShare: { [weak self] node, gesture in
|
||||
}, displayQuickShare: { [weak self] messageId, node, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.displayQuickShare(node: node, gesture: gesture)
|
||||
self.displayQuickShare(id: messageId, node: node, gesture: gesture)
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
|
||||
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
|
||||
|
||||
@ -7458,11 +7469,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||||
self.mode = mode
|
||||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
|
||||
return $0.updatedMode(mode)
|
||||
})
|
||||
}
|
||||
|
||||
func animateFromPreviewing(transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)) {
|
||||
guard let navigationController = self.effectiveNavigationController else {
|
||||
return
|
||||
}
|
||||
self.mode = .standard(.default)
|
||||
let completion = self.dismissPreviewing?(true)
|
||||
|
||||
let initialLayout = self.validLayout
|
||||
let initialFrame = self.view.convert(self.view.bounds, to: navigationController.view)
|
||||
|
||||
navigationController.pushViewController(self, animated: false)
|
||||
|
||||
let updatedLayout = self.validLayout
|
||||
let updatedFrame = self.view.frame
|
||||
|
||||
if let initialLayout, let updatedLayout, transition.isAnimated {
|
||||
let initialView = self.view.superview
|
||||
navigationController.view.addSubview(self.view)
|
||||
|
||||
self.view.clipsToBounds = true
|
||||
self.view.frame = initialFrame
|
||||
self.containerLayoutUpdated(initialLayout, transition: .immediate)
|
||||
self.containerLayoutUpdated(updatedLayout, transition: transition)
|
||||
|
||||
self.view.layer.animate(from: 14.0, to: updatedLayout.deviceMetrics.screenCornerRadius, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
|
||||
|
||||
transition.updateFrame(view: self.view, frame: updatedFrame, completion: { _ in
|
||||
initialView?.addSubview(self.view)
|
||||
self.view.clipsToBounds = false
|
||||
|
||||
completion?()
|
||||
})
|
||||
transition.updateCornerRadius(layer: self.view.layer, cornerRadius: 0.0)
|
||||
}
|
||||
|
||||
if let navigationBar = self.navigationBar {
|
||||
let nodes = [
|
||||
navigationBar.backButtonNode,
|
||||
navigationBar.backButtonArrow,
|
||||
navigationBar.badgeNode
|
||||
]
|
||||
for node in nodes {
|
||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
self.canReadHistory.set(true)
|
||||
|
||||
self.updateChatPresentationInterfaceState(transition: transition, interactive: false) { state in
|
||||
return state.updatedMode(self.mode)
|
||||
}
|
||||
}
|
||||
|
||||
var chatDisplayNode: ChatControllerNode {
|
||||
get {
|
||||
return super.displayNode as! ChatControllerNode
|
||||
|
@ -3453,7 +3453,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if recognizer.state == .ended {
|
||||
self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view))
|
||||
if case .standard(.previewing) = self.chatPresentationInterfaceState.mode {
|
||||
self.controller?.animateFromPreviewing()
|
||||
} else {
|
||||
self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
||||
let ignoreContentTouches: Bool = false
|
||||
let blurBackground: Bool = true
|
||||
let centerVertically: Bool
|
||||
let keepDefaultContentTouches: Bool
|
||||
|
||||
private weak var chatController: ChatControllerImpl?
|
||||
private weak var chatNode: ChatControllerNode?
|
||||
@ -59,13 +60,14 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
|
||||
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false, keepDefaultContentTouches: Bool = false) {
|
||||
self.chatController = chatController
|
||||
self.chatNode = chatNode
|
||||
self.engine = engine
|
||||
self.message = message
|
||||
self.selectAll = selectAll
|
||||
self.centerVertically = centerVertically
|
||||
self.keepDefaultContentTouches = keepDefaultContentTouches
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
|
@ -192,7 +192,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _ ,_ in
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
@ -2185,7 +2185,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _ ,_ in
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
|
||||
|
||||
|
@ -327,7 +327,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return CameraScreenImpl.TransitionIn(
|
||||
sourceView: sourceView,
|
||||
sourceRect: $0.sourceRect,
|
||||
sourceCornerRadius: $0.sourceCornerRadius
|
||||
sourceCornerRadius: $0.sourceCornerRadius,
|
||||
useFillAnimation: $0.useFillAnimation
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
@ -527,8 +528,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return StoryCameraTransitionInCoordinator(
|
||||
animateIn: { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.updateTransitionProgress(0.0, transition: .immediate)
|
||||
cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false)
|
||||
if transitionIn?.useFillAnimation == true {
|
||||
cameraController.animateIn()
|
||||
} else {
|
||||
cameraController.updateTransitionProgress(0.0, transition: .immediate)
|
||||
cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTransitionProgress: { [weak cameraController] transitionFraction in
|
||||
|
Loading…
x
Reference in New Issue
Block a user