Various improvements

This commit is contained in:
Ilya Laktyushin 2025-04-03 17:06:20 +04:00
parent 81d23edd72
commit c5168b8905
38 changed files with 1730 additions and 154 deletions

View File

@ -724,15 +724,18 @@ public struct StoryCameraTransitionIn {
public weak var sourceView: UIView? public weak var sourceView: UIView?
public let sourceRect: CGRect public let sourceRect: CGRect
public let sourceCornerRadius: CGFloat public let sourceCornerRadius: CGFloat
public let useFillAnimation: Bool
public init( public init(
sourceView: UIView, sourceView: UIView,
sourceRect: CGRect, sourceRect: CGRect,
sourceCornerRadius: CGFloat sourceCornerRadius: CGFloat,
useFillAnimation: Bool
) { ) {
self.sourceView = sourceView self.sourceView = sourceView
self.sourceRect = sourceRect self.sourceRect = sourceRect
self.sourceCornerRadius = sourceCornerRadius self.sourceCornerRadius = sourceCornerRadius
self.useFillAnimation = useFillAnimation
} }
} }

View File

@ -1022,7 +1022,7 @@ public protocol ChatController: ViewController {
var parentController: ViewController? { get set } var parentController: ViewController? { get set }
var customNavigationController: NavigationController? { get set } var customNavigationController: NavigationController? { get set }
var dismissPreviewing: (() -> Void)? { get set } var dismissPreviewing: ((Bool) -> (() -> Void))? { get set }
var purposefulAction: (() -> Void)? { get set } var purposefulAction: (() -> Void)? { get set }
var stateUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } var stateUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set }

View File

@ -298,15 +298,17 @@ public final class AvatarNode: ASDisplayNode {
let peerId: EnginePeer.Id? let peerId: EnginePeer.Id?
let resourceId: String? let resourceId: String?
let clipStyle: AvatarNodeClipStyle let clipStyle: AvatarNodeClipStyle
let displayDimensions: CGSize
init( init(
peerId: EnginePeer.Id?, peerId: EnginePeer.Id?,
resourceId: String?, resourceId: String?,
clipStyle: AvatarNodeClipStyle clipStyle: AvatarNodeClipStyle,
displayDimensions: CGSize
) { ) {
self.peerId = peerId self.peerId = peerId
self.resourceId = resourceId self.resourceId = resourceId
self.clipStyle = clipStyle self.clipStyle = clipStyle
self.displayDimensions = displayDimensions
} }
} }
@ -661,7 +663,8 @@ public final class AvatarNode: ASDisplayNode {
let params = Params( let params = Params(
peerId: peer?.id, peerId: peer?.id,
resourceId: smallProfileImage?.resource.id.stringRepresentation, resourceId: smallProfileImage?.resource.id.stringRepresentation,
clipStyle: clipStyle clipStyle: clipStyle,
displayDimensions: displayDimensions
) )
if self.params == params { if self.params == params {
return return

View File

@ -174,7 +174,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, attemptedNavigationToPrivateQuote: { _ in }, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: { }, forceUpdateWarpContents: {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))

View File

@ -1502,7 +1502,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.presentInGlobalOverlay(contextController) strongSelf.presentInGlobalOverlay(contextController)
} }
} else { } else {
var dismissPreviewingImpl: (() -> Void)? var dismissPreviewingImpl: ((Bool) -> (() -> Void))?
let source: ContextContentSource let source: ContextContentSource
if let location = location { if let location = location {
source = .location(ChatListContextLocationContentSource(controller: strongSelf, 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) 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.customNavigationController = strongSelf.navigationController as? NavigationController
chatController.canReadHistory.set(false) chatController.canReadHistory.set(false)
chatController.dismissPreviewing = { chatController.dismissPreviewing = { animateIn in
dismissPreviewingImpl?() return dismissPreviewingImpl?(animateIn) ?? {}
} }
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) 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) 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) strongSelf.presentInGlobalOverlay(contextController)
dismissPreviewingImpl = { [weak contextController] in dismissPreviewingImpl = { [weak self, weak contextController] animateIn in
contextController?.dismiss() 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, _, _): case let .forum(pinnedIndex, _, threadId, _, _):
@ -2794,7 +2805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
private weak var storyCameraTooltip: TooltipScreen? private weak var storyCameraTooltip: TooltipScreen?
fileprivate func openStoryCamera(fromList: Bool) { fileprivate func openStoryCamera(fromList: Bool, gesturePullOffset: CGFloat? = nil) {
guard !self.context.isFrozen else { guard !self.context.isFrozen else {
let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)
self.push(controller) self.push(controller)
@ -2920,8 +2931,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
cameraTransitionIn = StoryCameraTransitionIn( cameraTransitionIn = StoryCameraTransitionIn(
sourceView: transitionView, sourceView: transitionView,
sourceRect: transitionView.bounds, sourceRect: gesturePullOffset.flatMap({ transitionView.bounds.offsetBy(dx: -$0, dy: 0) }) ?? transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5 sourceCornerRadius: transitionView.bounds.height * 0.5,
useFillAnimation: gesturePullOffset != nil
) )
} }
} else { } else {
@ -2929,7 +2941,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
cameraTransitionIn = StoryCameraTransitionIn( cameraTransitionIn = StoryCameraTransitionIn(
sourceView: rightButtonView, sourceView: rightButtonView,
sourceRect: rightButtonView.bounds, 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() { 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 componentView.storyPeerAction = { [weak self] peer in
guard let self else { guard let self else {
return return

View File

@ -1748,7 +1748,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
} }
if peer.smallProfileImage != nil && overrideImage == nil { 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 { } 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)) 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))
} }

View File

@ -2184,6 +2184,7 @@ public protocol ContextExtractedContentSource: AnyObject {
var adjustContentHorizontally: Bool { get } var adjustContentHorizontally: Bool { get }
var adjustContentForSideInset: Bool { get } var adjustContentForSideInset: Bool { get }
var ignoreContentTouches: Bool { get } var ignoreContentTouches: Bool { get }
var keepDefaultContentTouches: Bool { get }
var blurBackground: Bool { get } var blurBackground: Bool { get }
var shouldBeDismissed: Signal<Bool, NoError> { get } var shouldBeDismissed: Signal<Bool, NoError> { get }
@ -2217,6 +2218,10 @@ public extension ContextExtractedContentSource {
var shouldBeDismissed: Signal<Bool, NoError> { var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false) return .single(false)
} }
var keepDefaultContentTouches: Bool {
return false
}
} }
public final class ContextControllerTakeControllerInfo { public final class ContextControllerTakeControllerInfo {

View File

@ -181,6 +181,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) { func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size))
guard self.controller.navigationController == nil else {
return
}
self.controller.containerLayoutUpdated( self.controller.containerLayoutUpdated(
ContainerViewLayout( ContainerViewLayout(
size: size, size: size,
@ -376,7 +379,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
if let result = contentNode.containingItem.customHitTest?(contentPoint) { if let result = contentNode.containingItem.customHitTest?(contentPoint) {
return result return result
} else if let result = contentNode.containingItem.contentHitTest(contentPoint, with: event) { } 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 return result
} else if contentNode.containingItem.contentRect.contains(contentPoint) { } else if contentNode.containingItem.contentRect.contains(contentPoint) {
return contentNode.containingItem.contentView return contentNode.containingItem.contentView

View File

@ -3336,7 +3336,8 @@ public func stickerMediaPickerController(
transitionIn: CameraScreenImpl.TransitionIn( transitionIn: CameraScreenImpl.TransitionIn(
sourceView: cameraHolder.parentView, sourceView: cameraHolder.parentView,
sourceRect: cameraHolder.parentView.bounds, sourceRect: cameraHolder.parentView.bounds,
sourceCornerRadius: 0.0 sourceCornerRadius: 0.0,
useFillAnimation: false
), ),
transitionOut: { _ in transitionOut: { _ in
return CameraScreenImpl.TransitionOut( return CameraScreenImpl.TransitionOut(
@ -3453,7 +3454,8 @@ public func avatarMediaPickerController(
transitionIn: CameraScreenImpl.TransitionIn( transitionIn: CameraScreenImpl.TransitionIn(
sourceView: cameraHolder.parentView, sourceView: cameraHolder.parentView,
sourceRect: cameraHolder.parentView.bounds, sourceRect: cameraHolder.parentView.bounds,
sourceCornerRadius: 0.0 sourceCornerRadius: 0.0,
useFillAnimation: false
), ),
transitionOut: { _ in transitionOut: { _ in
return CameraScreenImpl.TransitionOut( return CameraScreenImpl.TransitionOut(

View File

@ -650,6 +650,9 @@ private extension StarsContext.State.Transaction {
if (apiFlags & (1 << 19)) != 0 { if (apiFlags & (1 << 19)) != 0 {
flags.insert(.isPaidMessage) flags.insert(.isPaidMessage)
} }
if (apiFlags & (1 << 21)) != 0 {
flags.insert(.isBusinessTransfer)
}
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod let _ = subscriptionPeriod
@ -702,6 +705,7 @@ public final class StarsContext {
public static let isReaction = Flags(rawValue: 1 << 5) public static let isReaction = Flags(rawValue: 1 << 5)
public static let isStarGiftUpgrade = Flags(rawValue: 1 << 6) public static let isStarGiftUpgrade = Flags(rawValue: 1 << 6)
public static let isPaidMessage = Flags(rawValue: 1 << 7) public static let isPaidMessage = Flags(rawValue: 1 << 7)
public static let isBusinessTransfer = Flags(rawValue: 1 << 8)
} }
public enum Peer: Equatable { public enum Peer: Equatable {

View File

@ -1668,15 +1668,18 @@ public class CameraScreenImpl: ViewController, CameraScreen {
public weak var sourceView: UIView? public weak var sourceView: UIView?
public let sourceRect: CGRect public let sourceRect: CGRect
public let sourceCornerRadius: CGFloat public let sourceCornerRadius: CGFloat
public let useFillAnimation: Bool
public init( public init(
sourceView: UIView, sourceView: UIView,
sourceRect: CGRect, sourceRect: CGRect,
sourceCornerRadius: CGFloat sourceCornerRadius: CGFloat,
useFillAnimation: Bool
) { ) {
self.sourceView = sourceView self.sourceView = sourceView
self.sourceRect = sourceRect self.sourceRect = sourceRect
self.sourceCornerRadius = sourceCornerRadius 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 { if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
if case .story = controller.mode { if transitionIn.useFillAnimation {
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width 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.transitionDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15)
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) let transitionMaskView = UIView()
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) transitionMaskView.frame = self.view.bounds
self.previewContainerView.layer.animate( self.view.mask = transitionMaskView
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 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 colorFillView = UIView()
let sourceCenter = sourceInnerFrame.center colorFillView.backgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor
self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in colorFillView.frame = self.view.bounds
self.requestUpdateLayout(hasAppeared: true, transition: .immediate) colorFillView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}) self.view.addSubview(colorFillView)
var sourceBounds = self.mainPreviewView.bounds let iconLayer = SimpleLayer()
if let holder = controller.holder { iconLayer.contents = generateAddIcon(color: self.presentationData.theme.list.itemCheckColors.foregroundColor)?.cgImage
sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size)) 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) iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false)
self.mainPreviewView.transform = CGAffineTransform.identity labelLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false)
self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.mainPreviewContainerView.addSubview(self.mainPreviewView) transitionCircleLayer.animateScale(from: sourceLocalFrame.width / 320.0, to: 6.0, duration: 0.6, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
Queue.mainQueue().justDispatch { self.view.mask = nil
self.animatedIn = true colorFillView.removeFromSuperview()
}
}) })
} } else {
if case .story = controller.mode {
if let view = self.componentHost.view { let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
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) self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) 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) self.dismiss(animated: false)
} }
} }
public func animateIn() {
self.node.animateIn()
}
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
if let layout = self.validLayout, layout.metrics.isTablet { if let layout = self.validLayout, layout.metrics.isTablet {
@ -4009,3 +4061,37 @@ private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoi
return position 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()
})
}

View File

@ -1480,6 +1480,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
updatedShareButtonNode.pressed = { [weak strongSelf] in updatedShareButtonNode.pressed = { [weak strongSelf] in
strongSelf?.shareButtonPressed() 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 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) 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 private var playedSwipeToReplyHaptic = false
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0

View File

@ -5804,7 +5804,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) { private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
if let item = self.item { if let item = self.item {
item.controllerInteraction.displayQuickShare(node, gesture) item.controllerInteraction.displayQuickShare(item.message.id, node, gesture)
} }
} }

View File

@ -1012,6 +1012,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
updatedShareButtonNode.pressed = { [weak strongSelf] in updatedShareButtonNode.pressed = { [weak strongSelf] in
strongSelf?.shareButtonPressed() 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 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) 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 private var playedSwipeToReplyHaptic = false
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0

View File

@ -645,7 +645,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, attemptedNavigationToPrivateQuote: { _ in }, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: { }, forceUpdateWarpContents: {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction

View File

@ -500,7 +500,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, attemptedNavigationToPrivateQuote: { _ in }, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: { }, forceUpdateWarpContents: {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))

View File

@ -289,7 +289,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
backgroundSize.height += verticalInset backgroundSize.height += verticalInset
let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0 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 += titleLayout.size.height
backgroundSize.height += verticalSpacing backgroundSize.height += verticalSpacing
@ -297,7 +297,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
backgroundSize.height += subtitleLayout.size.height backgroundSize.height += subtitleLayout.size.height
backgroundSize.height += verticalSpacing + paragraphSpacing 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 maxTitleWidth: CGFloat = 0.0
var maxValueWidth: CGFloat = 0.0 var maxValueWidth: CGFloat = 0.0
@ -389,7 +389,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
groupsValueLayoutAndApply = nil 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 let disclaimerText: NSMutableAttributedString
if let verification = item.verification { if let verification = item.verification {

View File

@ -24,6 +24,7 @@ swift_library(
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/PresentationDataUtils", "//submodules/PresentationDataUtils",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AvatarNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -8,27 +8,38 @@ import TelegramCore
import TextFormat import TextFormat
import TelegramPresentationData import TelegramPresentationData
import MultilineTextComponent import MultilineTextComponent
import LottieComponent
import AccountContext import AccountContext
import ViewControllerComponent import ViewControllerComponent
import AvatarNode import AvatarNode
import ComponentDisplayAdapters import ComponentDisplayAdapters
private let largeCircleSize: CGFloat = 16.0
private let smallCircleSize: CGFloat = 8.0
private final class QuickShareScreenComponent: Component { private final class QuickShareScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let sourceNode: ASDisplayNode let sourceNode: ASDisplayNode
let gesture: ContextGesture let gesture: ContextGesture
let openPeer: (EnginePeer.Id) -> Void
let completion: (EnginePeer.Id) -> Void
let ready: Promise<Bool>
init( init(
context: AccountContext, context: AccountContext,
sourceNode: ASDisplayNode, sourceNode: ASDisplayNode,
gesture: ContextGesture gesture: ContextGesture,
openPeer: @escaping (EnginePeer.Id) -> Void,
completion: @escaping (EnginePeer.Id) -> Void,
ready: Promise<Bool>
) { ) {
self.context = context self.context = context
self.sourceNode = sourceNode self.sourceNode = sourceNode
self.gesture = gesture self.gesture = gesture
self.openPeer = openPeer
self.completion = completion
self.ready = ready
} }
static func ==(lhs: QuickShareScreenComponent, rhs: QuickShareScreenComponent) -> Bool { static func ==(lhs: QuickShareScreenComponent, rhs: QuickShareScreenComponent) -> Bool {
@ -36,10 +47,16 @@ private final class QuickShareScreenComponent: Component {
} }
final class View: UIView { final class View: UIView {
private let backgroundShadowLayer: SimpleLayer
private let backgroundView: BlurredBackgroundView private let backgroundView: BlurredBackgroundView
private let backgroundTintView: UIView private let backgroundTintView: UIView
private let containerView: 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 items: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var isUpdating: Bool = false private var isUpdating: Bool = false
@ -55,12 +72,30 @@ private final class QuickShareScreenComponent: Component {
private var initialContinueGesturePoint: CGPoint? private var initialContinueGesturePoint: CGPoint?
private var didMoveFromInitialGesturePoint = false private var didMoveFromInitialGesturePoint = false
private let hapticFeedback = HapticFeedback()
override init(frame: CGRect) { override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundView.clipsToBounds = true self.backgroundView.clipsToBounds = true
self.backgroundTintView = UIView() self.backgroundTintView = UIView()
self.backgroundTintView.clipsToBounds = true 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 = UIView()
self.containerView.clipsToBounds = true self.containerView.clipsToBounds = true
@ -68,6 +103,7 @@ private final class QuickShareScreenComponent: Component {
self.addSubview(self.backgroundView) self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.backgroundTintView) self.backgroundView.addSubview(self.backgroundTintView)
self.layer.addSublayer(self.backgroundShadowLayer)
self.addSubview(self.containerView) self.addSubview(self.containerView)
} }
@ -80,9 +116,40 @@ private final class QuickShareScreenComponent: Component {
} }
func animateIn() { func animateIn() {
self.hapticFeedback.impact()
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) 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.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) 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) { Queue.mainQueue().after(0.3) {
self.containerView.clipsToBounds = false self.containerView.clipsToBounds = false
@ -98,21 +165,45 @@ private final class QuickShareScreenComponent: Component {
} }
func highlightGestureMoved(location: CGPoint) { func highlightGestureMoved(location: CGPoint) {
var selectedPeerId: EnginePeer.Id?
for (peerId, view) in self.items { for (peerId, view) in self.items {
guard let view = view.view else { guard let view = view.view else {
continue continue
} }
if view.frame.contains(location) { if view.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) {
self.selectedPeerId = peerId selectedPeerId = peerId
self.state?.updated(transition: .spring(duration: 0.3))
break 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) { func highlightGestureFinished(performAction: Bool) {
if let selectedPeerId = self.selectedPeerId, performAction { 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 { self.animateOut {
if let controller = self.environment?.controller() { if let controller = self.environment?.controller() {
controller.dismiss() controller.dismiss()
@ -158,6 +249,7 @@ private final class QuickShareScreenComponent: Component {
} else { } else {
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()
} }
component.ready.set(.single(true))
}) })
component.gesture.externalUpdated = { [weak self] view, point in component.gesture.externalUpdated = { [weak self] view, point in
@ -216,7 +308,19 @@ private final class QuickShareScreenComponent: Component {
let padding: CGFloat = 5.0 let padding: CGFloat = 5.0
let spacing: CGFloat = 7.0 let spacing: CGFloat = 7.0
let itemSize = CGSize(width: 38.0, height: 38.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) var itemFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: itemSize)
if let peers = self.peers { if let peers = self.peers {
@ -236,13 +340,18 @@ private final class QuickShareScreenComponent: Component {
isFocused = peer.id == selectedPeerId 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( let _ = componentView.update(
transition: componentTransition, transition: componentTransition,
component: AnyComponent( component: AnyComponent(
ItemComponent( ItemComponent(
context: component.context, context: component.context,
theme: environment.theme, theme: environment.theme,
strings: environment.strings,
peer: peer, 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 isFocused: isFocused
) )
), ),
@ -253,29 +362,29 @@ private final class QuickShareScreenComponent: Component {
if view.superview == nil { if view.superview == nil {
self.containerView.addSubview(view) 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.containerView.layer.cornerRadius = size.height / 2.0
self.backgroundView.layer.cornerRadius = size.height / 2.0 self.backgroundView.layer.cornerRadius = size.height / 2.0
self.backgroundTintView.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.backgroundView, frame: contentRect)
transition.setFrame(view: self.containerView, 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)) 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 return availableSize
} }
} }
@ -293,17 +402,29 @@ public class QuickShareScreen: ViewControllerComponentContainer {
private var processedDidAppear: Bool = false private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false private var processedDidDisappear: Bool = false
private let readyValue = Promise<Bool>()
override public var ready: Promise<Bool> {
return self.readyValue
}
public init( public init(
context: AccountContext, context: AccountContext,
sourceNode: ASDisplayNode, sourceNode: ASDisplayNode,
gesture: ContextGesture gesture: ContextGesture,
openPeer: @escaping (EnginePeer.Id) -> Void,
completion: @escaping (EnginePeer.Id) -> Void
) { ) {
let componentReady = Promise<Bool>()
super.init( super.init(
context: context, context: context,
component: QuickShareScreenComponent( component: QuickShareScreenComponent(
context: context, context: context,
sourceNode: sourceNode, sourceNode: sourceNode,
gesture: gesture gesture: gesture,
openPeer: openPeer,
completion: completion,
ready: componentReady
), ),
navigationBarAppearance: .none, navigationBarAppearance: .none,
statusBarStyle: .ignore, statusBarStyle: .ignore,
@ -311,6 +432,8 @@ public class QuickShareScreen: ViewControllerComponentContainer {
updatedPresentationData: nil updatedPresentationData: nil
) )
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal
self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
} }
required public init(coder aDecoder: NSCoder) { required public init(coder aDecoder: NSCoder) {
@ -360,18 +483,24 @@ public class QuickShareScreen: ViewControllerComponentContainer {
private final class ItemComponent: Component { private final class ItemComponent: Component {
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings
let peer: EnginePeer let peer: EnginePeer
let safeInsets: UIEdgeInsets
let isFocused: Bool? let isFocused: Bool?
init( init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings,
peer: EnginePeer, peer: EnginePeer,
safeInsets: UIEdgeInsets,
isFocused: Bool? isFocused: Bool?
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.strings = strings
self.peer = peer self.peer = peer
self.safeInsets = safeInsets
self.isFocused = isFocused self.isFocused = isFocused
} }
@ -379,6 +508,9 @@ private final class ItemComponent: Component {
if lhs.peer != rhs.peer { if lhs.peer != rhs.peer {
return false return false
} }
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.isFocused != rhs.isFocused { if lhs.isFocused != rhs.isFocused {
return false return false
} }
@ -386,7 +518,7 @@ private final class ItemComponent: Component {
} }
final class View: UIView { final class View: UIView {
private let avatarNode: AvatarNode fileprivate let avatarNode: AvatarNode
private let backgroundNode: NavigationBackgroundNode private let backgroundNode: NavigationBackgroundNode
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
@ -415,11 +547,13 @@ private final class ItemComponent: Component {
self.isUpdating = false self.isUpdating = false
} }
let size = CGSize(width: 60.0, height: 60.0)
var title = component.peer.compactDisplayTitle var title = component.peer.compactDisplayTitle
var overrideImage: AvatarNodeImageOverride? var overrideImage: AvatarNodeImageOverride?
if component.peer.id == component.context.account.peerId { if component.peer.id == component.context.account.peerId {
overrideImage = .savedMessagesIcon overrideImage = .savedMessagesIcon
title = "Saved Messages" title = component.strings.DialogList_SavedMessages
} }
self.avatarNode.setPeer( self.avatarNode.setPeer(
@ -430,22 +564,16 @@ private final class ItemComponent: Component {
synchronousLoad: true synchronousLoad: true
) )
self.avatarNode.view.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) self.avatarNode.view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.avatarNode.view.bounds = CGRect(origin: .zero, size: availableSize) self.avatarNode.view.bounds = CGRect(origin: .zero, size: size)
self.avatarNode.updateSize(size: availableSize) self.avatarNode.updateSize(size: size)
var scale: CGFloat = 1.0
var alpha: CGFloat = 1.0
var textAlpha: CGFloat = 0.0 var textAlpha: CGFloat = 0.0
var textOffset: CGFloat = 6.0 var textOffset: CGFloat = 6.0
if let isFocused = component.isFocused { if let isFocused = component.isFocused {
scale = isFocused ? 1.1 : 1.0
alpha = isFocused ? 1.0 : 0.6
textAlpha = isFocused ? 1.0 : 0.0 textAlpha = isFocused ? 1.0 : 0.0
textOffset = isFocused ? 0.0 : 6.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( let textSize = self.text.update(
transition: .immediate, transition: .immediate,
@ -459,10 +587,27 @@ private final class ItemComponent: Component {
if textView.superview == nil { if textView.superview == nil {
self.addSubview(textView) 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) 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) transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame)
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: .immediate) 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) 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) 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) 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))
}

View File

@ -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)
}
}
}
}

View File

@ -280,7 +280,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let attemptedNavigationToPrivateQuote: (Peer?) -> Void public let attemptedNavigationToPrivateQuote: (Peer?) -> Void
public let forceUpdateWarpContents: () -> Void public let forceUpdateWarpContents: () -> Void
public let playShakeAnimation: () -> 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 canPlayMedia: Bool = false
public var hiddenMedia: [MessageId: [Media]] = [:] public var hiddenMedia: [MessageId: [Media]] = [:]
@ -440,7 +440,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void,
forceUpdateWarpContents: @escaping () -> Void, forceUpdateWarpContents: @escaping () -> Void,
playShakeAnimation: @escaping () -> Void, playShakeAnimation: @escaping () -> Void,
displayQuickShare: @escaping (ASDisplayNode, ContextGesture) -> Void, displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void,
automaticMediaDownloadSettings: MediaAutoDownloadSettings, automaticMediaDownloadSettings: MediaAutoDownloadSettings,
pollActionState: ChatInterfacePollActionState, pollActionState: ChatInterfacePollActionState,
stickerSettings: ChatInterfaceStickerSettings, stickerSettings: ChatInterfaceStickerSettings,

View File

@ -746,6 +746,7 @@ public final class ChatListHeaderComponent: Component {
private var storyPeerList: ComponentView<Empty>? private var storyPeerList: ComponentView<Empty>?
public var storyPeerAction: ((EnginePeer?) -> Void)? public var storyPeerAction: ((EnginePeer?) -> Void)?
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)? public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
public var storyComposeAction: ((CGFloat) -> Void)?
private var effectiveContentView: ContentView? { private var effectiveContentView: ContentView? {
return self.secondaryContentView ?? self.primaryContentView return self.secondaryContentView ?? self.primaryContentView
@ -977,6 +978,12 @@ public final class ChatListHeaderComponent: Component {
return return
} }
self.component?.toggleIsLocked() self.component?.toggleIsLocked()
},
composeAction: { [weak self] offset in
guard let self else {
return
}
self.storyComposeAction?(offset)
} }
)), )),
environment: {}, environment: {},

View File

@ -130,8 +130,6 @@ final class GiftOptionsScreenComponent: Component {
private let premiumTitle = ComponentView<Empty>() private let premiumTitle = ComponentView<Empty>()
private let premiumDescription = ComponentView<Empty>() private let premiumDescription = ComponentView<Empty>()
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:] private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
private var inProgressPremiumGift: String?
private let purchaseDisposable = MetaDisposable()
private let starsTitle = ComponentView<Empty>() private let starsTitle = ComponentView<Empty>()
private let starsDescription = ComponentView<Empty>() private let starsDescription = ComponentView<Empty>()
@ -251,7 +249,6 @@ final class GiftOptionsScreenComponent: Component {
deinit { deinit {
self.starsStateDisposable?.dispose() self.starsStateDisposable?.dispose()
self.purchaseDisposable.dispose()
} }
func scrollToTop() { func scrollToTop() {
@ -262,21 +259,7 @@ final class GiftOptionsScreenComponent: Component {
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) 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) { private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component else { guard let environment = self.environment, let component = self.component else {
return return
@ -725,12 +708,8 @@ final class GiftOptionsScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0 let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left
let _ = bottomContentInset
let _ = sectionSpacing
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled || state.disallowedGifts?.contains(.premium) == true let isPremiumDisabled = premiumConfiguration.isPremiumDisabled || state.disallowedGifts?.contains(.premium) == true
@ -1056,7 +1035,7 @@ final class GiftOptionsScreenComponent: Component {
color: .purple color: .purple
) )
}, },
isLoading: self.inProgressPremiumGift == product.id isLoading: false
) )
), ),
effectAlignment: .center, effectAlignment: .center,

View File

@ -3744,7 +3744,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, attemptedNavigationToPrivateQuote: { _ in }, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: { }, forceUpdateWarpContents: {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in
@ -10068,7 +10068,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
cameraTransitionIn = StoryCameraTransitionIn( cameraTransitionIn = StoryCameraTransitionIn(
sourceView: rightButton.view, sourceView: rightButton.view,
sourceRect: rightButton.view.bounds, sourceRect: rightButton.view.bounds,
sourceCornerRadius: rightButton.view.bounds.height * 0.5 sourceCornerRadius: rightButton.view.bounds.height * 0.5,
useFillAnimation: false
) )
} }

View File

@ -1,4 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") 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( swift_library(
name = "StoryPeerListComponent", name = "StoryPeerListComponent",
@ -8,9 +48,13 @@ swift_library(
]), ]),
copts = [ copts = [
"-warnings-as-errors", "-warnings-as-errors",
],
data = [
":StoryPeerListBundle",
], ],
deps = [ deps = [
"//submodules/Display", "//submodules/Display",
"//submodules/MetalEngine",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",

View File

@ -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);
}

View File

@ -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()
})
}

View File

@ -60,6 +60,7 @@ public final class StoryPeerListComponent: Component {
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public let openStatusSetup: (UIView) -> Void public let openStatusSetup: (UIView) -> Void
public let lockAction: () -> Void public let lockAction: () -> Void
public let composeAction: (CGFloat) -> Void
public init( public init(
externalState: ExternalState, externalState: ExternalState,
@ -81,7 +82,8 @@ public final class StoryPeerListComponent: Component {
peerAction: @escaping (EnginePeer?) -> Void, peerAction: @escaping (EnginePeer?) -> Void,
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void, contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void,
openStatusSetup: @escaping (UIView) -> Void, openStatusSetup: @escaping (UIView) -> Void,
lockAction: @escaping () -> Void lockAction: @escaping () -> Void,
composeAction: @escaping (CGFloat) -> Void
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -103,6 +105,7 @@ public final class StoryPeerListComponent: Component {
self.contextPeerAction = contextPeerAction self.contextPeerAction = contextPeerAction
self.openStatusSetup = openStatusSetup self.openStatusSetup = openStatusSetup
self.lockAction = lockAction self.lockAction = lockAction
self.composeAction = composeAction
} }
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
@ -162,7 +165,6 @@ public final class StoryPeerListComponent: Component {
private final class VisibleItem { private final class VisibleItem {
let view = ComponentView<Empty>() let view = ComponentView<Empty>()
var hasBlur: Bool = false
init() { init() {
} }
@ -360,6 +362,8 @@ public final class StoryPeerListComponent: Component {
private var sharedBlurEffect: NSObject? private var sharedBlurEffect: NSObject?
private var willComposeOnRelease = false
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.collapsedButton = HighlightableButton() self.collapsedButton = HighlightableButton()
@ -561,14 +565,62 @@ public final class StoryPeerListComponent: Component {
public func scrollViewDidScroll(_ scrollView: UIScrollView) { public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling { if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate) 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) { private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let itemLayout = self.itemLayout else { guard let component = self.component, let itemLayout = self.itemLayout else {
return return
} }
let titleIconSpacing: CGFloat = 4.0 let titleIconSpacing: CGFloat = 4.0
let titleIndicatorSpacing: CGFloat = 8.0 let titleIndicatorSpacing: CGFloat = 8.0
@ -1072,6 +1124,11 @@ public final class StoryPeerListComponent: Component {
totalCount = itemSet.storyCount totalCount = itemSet.storyCount
unseenCount = itemSet.unseenCount 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( let _ = visibleItem.view.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent(StoryPeerListItemComponent( component: AnyComponent(StoryPeerListItemComponent(
@ -1090,6 +1147,7 @@ public final class StoryPeerListComponent: Component {
expandEffectFraction: collapsedState.expandEffectFraction, expandEffectFraction: collapsedState.expandEffectFraction,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance, rightNeighborDistance: rightNeighborDistance,
composeContentOffset: composeContentOffset,
action: component.peerAction, action: component.peerAction,
contextGesture: component.contextPeerAction contextGesture: component.contextPeerAction
)), )),
@ -1228,6 +1286,7 @@ public final class StoryPeerListComponent: Component {
expandEffectFraction: collapsedState.expandEffectFraction, expandEffectFraction: collapsedState.expandEffectFraction,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance, rightNeighborDistance: rightNeighborDistance,
composeContentOffset: nil,
action: component.peerAction, action: component.peerAction,
contextGesture: component.contextPeerAction contextGesture: component.contextPeerAction
)), )),

View File

@ -380,6 +380,7 @@ public final class StoryPeerListItemComponent: Component {
public let expandEffectFraction: CGFloat public let expandEffectFraction: CGFloat
public let leftNeighborDistance: CGPoint? public let leftNeighborDistance: CGPoint?
public let rightNeighborDistance: CGPoint? public let rightNeighborDistance: CGPoint?
public let composeContentOffset: CGFloat?
public let action: (EnginePeer) -> Void public let action: (EnginePeer) -> Void
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
@ -399,6 +400,7 @@ public final class StoryPeerListItemComponent: Component {
expandEffectFraction: CGFloat, expandEffectFraction: CGFloat,
leftNeighborDistance: CGPoint?, leftNeighborDistance: CGPoint?,
rightNeighborDistance: CGPoint?, rightNeighborDistance: CGPoint?,
composeContentOffset: CGFloat?,
action: @escaping (EnginePeer) -> Void, action: @escaping (EnginePeer) -> Void,
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
) { ) {
@ -417,6 +419,7 @@ public final class StoryPeerListItemComponent: Component {
self.expandEffectFraction = expandEffectFraction self.expandEffectFraction = expandEffectFraction
self.leftNeighborDistance = leftNeighborDistance self.leftNeighborDistance = leftNeighborDistance
self.rightNeighborDistance = rightNeighborDistance self.rightNeighborDistance = rightNeighborDistance
self.composeContentOffset = composeContentOffset
self.action = action self.action = action
self.contextGesture = contextGesture self.contextGesture = contextGesture
} }
@ -467,6 +470,9 @@ public final class StoryPeerListItemComponent: Component {
if lhs.rightNeighborDistance != rhs.rightNeighborDistance { if lhs.rightNeighborDistance != rhs.rightNeighborDistance {
return false return false
} }
if lhs.composeContentOffset != rhs.composeContentOffset {
return false
}
return true return true
} }
@ -479,6 +485,7 @@ public final class StoryPeerListItemComponent: Component {
private let button: HighlightTrackingButton private let button: HighlightTrackingButton
fileprivate var composeLayer: StoryComposeLayer?
fileprivate let avatarContent: PortalSourceView fileprivate let avatarContent: PortalSourceView
private let avatarContainer: UIView private let avatarContainer: UIView
private let avatarBackgroundContainer: UIView private let avatarBackgroundContainer: UIView
@ -494,12 +501,11 @@ public final class StoryPeerListItemComponent: Component {
private let indicatorShapeSeenLayer: SimpleShapeLayer private let indicatorShapeSeenLayer: SimpleShapeLayer
private let indicatorShapeUnseenLayer: SimpleShapeLayer private let indicatorShapeUnseenLayer: SimpleShapeLayer
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private let composeTitle = ComponentView<Empty>()
private var component: StoryPeerListItemComponent? private var component: StoryPeerListItemComponent?
private weak var componentState: EmptyComponentState? private weak var componentState: EmptyComponentState?
private var demoLoading = false
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.backgroundContainer = UIView() self.backgroundContainer = UIView()
self.backgroundContainer.isUserInteractionEnabled = false 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.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) 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 return availableSize
} }
} }

View File

@ -418,7 +418,7 @@ extension ChatControllerImpl {
} }
} }
strongSelf.dismissPreviewing?() let _ = strongSelf.dismissPreviewing?(false)
} }
})) }))
case .replyThread: case .replyThread:

View File

@ -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 let source: ContextContentSource
if let location = location { 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))) source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
} else { } 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) self.canReadHistory.set(false)

View File

@ -1,15 +1,52 @@
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import SwiftSignalKit
import TelegramCore
import ContextUI import ContextUI
import QuickShareScreen import QuickShareScreen
extension ChatControllerImpl { extension ChatControllerImpl {
func displayQuickShare(node: ASDisplayNode, gesture: ContextGesture) { func displayQuickShare(id: EngineMessage.Id, node: ASDisplayNode, gesture: ContextGesture) {
guard !"".isEmpty else { let controller = QuickShareScreen(
return context: self.context,
} sourceNode: node,
let controller = QuickShareScreen(context: self.context, sourceNode: node, gesture: gesture) 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) self.presentInGlobalOverlay(controller)
} }
} }

View File

@ -250,7 +250,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var botStart: ChatControllerInitialBotStart? var botStart: ChatControllerInitialBotStart?
var attachBotStart: ChatControllerInitialAttachBotStart? var attachBotStart: ChatControllerInitialAttachBotStart?
var botAppStart: ChatControllerInitialBotAppStart? var botAppStart: ChatControllerInitialBotAppStart?
let mode: ChatControllerPresentationMode var mode: ChatControllerPresentationMode
let peerDisposable = MetaDisposable() let peerDisposable = MetaDisposable()
let titleDisposable = MetaDisposable() let titleDisposable = MetaDisposable()
@ -575,7 +575,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)? var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)?
public var purposefulAction: (() -> Void)? public var purposefulAction: (() -> Void)?
public var dismissPreviewing: (() -> Void)? public var dismissPreviewing: ((Bool) -> (() -> Void))?
var updatedClosedPinnedMessageId: ((MessageId) -> Void)? var updatedClosedPinnedMessageId: ((MessageId) -> Void)?
var requestedUnpinAllMessages: ((Int, MessageId) -> Void)? var requestedUnpinAllMessages: ((Int, MessageId) -> Void)?
@ -893,6 +893,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false 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 mode = params.mode
let displayVoiceMessageDiscardAlert: () -> Bool = { [weak self] in let displayVoiceMessageDiscardAlert: () -> Bool = { [weak self] in
@ -1360,7 +1367,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let self else { guard let self else {
return 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 }, openUrl: { [weak self] url in
self?.openUrl(url, concealed: false, skipConcealedAlert: isLocation, message: nil) self?.openUrl(url, concealed: false, skipConcealedAlert: isLocation, message: nil)
}, openPeer: { [weak self] peer, navigation in }, openPeer: { [weak self] peer, navigation in
@ -4850,11 +4861,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
self.playShakeAnimation() self.playShakeAnimation()
}, displayQuickShare: { [weak self] node, gesture in }, displayQuickShare: { [weak self] messageId, node, gesture in
guard let self else { guard let self else {
return 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)) }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
@ -7458,11 +7469,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
self.mode = mode
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
return $0.updatedMode(mode) 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 { var chatDisplayNode: ChatControllerNode {
get { get {
return super.displayNode as! ChatControllerNode return super.displayNode as! ChatControllerNode

View File

@ -3453,7 +3453,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == .ended { 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))
}
} }
} }

View File

@ -33,6 +33,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
let ignoreContentTouches: Bool = false let ignoreContentTouches: Bool = false
let blurBackground: Bool = true let blurBackground: Bool = true
let centerVertically: Bool let centerVertically: Bool
let keepDefaultContentTouches: Bool
private weak var chatController: ChatControllerImpl? private weak var chatController: ChatControllerImpl?
private weak var chatNode: ChatControllerNode? private weak var chatNode: ChatControllerNode?
@ -59,13 +60,14 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|> distinctUntilChanged |> 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.chatController = chatController
self.chatNode = chatNode self.chatNode = chatNode
self.engine = engine self.engine = engine
self.message = message self.message = message
self.selectAll = selectAll self.selectAll = selectAll
self.centerVertically = centerVertically self.centerVertically = centerVertically
self.keepDefaultContentTouches = keepDefaultContentTouches
} }
func takeView() -> ContextControllerTakeViewInfo? { func takeView() -> ContextControllerTakeViewInfo? {

View File

@ -192,7 +192,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, attemptedNavigationToPrivateQuote: { _ in }, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: { }, forceUpdateWarpContents: {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.dimNode = ASDisplayNode() self.dimNode = ASDisplayNode()

View File

@ -2185,7 +2185,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, attemptedNavigationToPrivateQuote: { _ in }, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: { }, forceUpdateWarpContents: {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))

View File

@ -327,7 +327,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return CameraScreenImpl.TransitionIn( return CameraScreenImpl.TransitionIn(
sourceView: sourceView, sourceView: sourceView,
sourceRect: $0.sourceRect, sourceRect: $0.sourceRect,
sourceCornerRadius: $0.sourceCornerRadius sourceCornerRadius: $0.sourceCornerRadius,
useFillAnimation: $0.useFillAnimation
) )
} else { } else {
return nil return nil
@ -527,8 +528,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return StoryCameraTransitionInCoordinator( return StoryCameraTransitionInCoordinator(
animateIn: { [weak cameraController] in animateIn: { [weak cameraController] in
if let cameraController { if let cameraController {
cameraController.updateTransitionProgress(0.0, transition: .immediate) if transitionIn?.useFillAnimation == true {
cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false) cameraController.animateIn()
} else {
cameraController.updateTransitionProgress(0.0, transition: .immediate)
cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false)
}
} }
}, },
updateTransitionProgress: { [weak cameraController] transitionFraction in updateTransitionProgress: { [weak cameraController] transitionFraction in