mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Various improvements
This commit is contained in:
parent
81d23edd72
commit
c5168b8905
@ -724,15 +724,18 @@ public struct StoryCameraTransitionIn {
|
|||||||
public weak var sourceView: UIView?
|
public 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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",
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
@ -0,0 +1,372 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ComponentFlow
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import AppBundle
|
||||||
|
import ViewControllerComponent
|
||||||
|
import AccountContext
|
||||||
|
import MultilineTextComponent
|
||||||
|
import AvatarNode
|
||||||
|
import Markdown
|
||||||
|
import LottieComponent
|
||||||
|
|
||||||
|
private final class QuickShareToastScreenComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let peer: EnginePeer
|
||||||
|
let sourceFrame: CGRect
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
peer: EnginePeer,
|
||||||
|
sourceFrame: CGRect,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.peer = peer
|
||||||
|
self.sourceFrame = sourceFrame
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: QuickShareToastScreenComponent, rhs: QuickShareToastScreenComponent) -> Bool {
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sourceFrame != rhs.sourceFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let contentView: UIView
|
||||||
|
private let backgroundView: BlurredBackgroundView
|
||||||
|
|
||||||
|
private let avatarNode: AvatarNode
|
||||||
|
private let animation = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private let content = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var isUpdating: Bool = false
|
||||||
|
private var component: QuickShareToastScreenComponent?
|
||||||
|
private var environment: EnvironmentType?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
private var doneTimer: Foundation.Timer?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||||
|
self.contentView = UIView()
|
||||||
|
self.contentView.isUserInteractionEnabled = false
|
||||||
|
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
self.backgroundView.addSubview(self.contentView)
|
||||||
|
self.contentView.addSubview(self.avatarNode.view)
|
||||||
|
|
||||||
|
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture)))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.doneTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
if !self.backgroundView.frame.contains(point) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action()
|
||||||
|
self.environment?.controller()?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
|
||||||
|
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
|
||||||
|
|
||||||
|
let x1 = sourcePoint.x
|
||||||
|
let y1 = sourcePoint.y
|
||||||
|
let x2 = midPoint.x
|
||||||
|
let y2 = midPoint.y
|
||||||
|
let x3 = targetPosition.x
|
||||||
|
let y3 = targetPosition.y
|
||||||
|
|
||||||
|
var keyframes: [CGPoint] = []
|
||||||
|
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
|
||||||
|
for i in 0 ..< 10 {
|
||||||
|
let k = CGFloat(i) / CGFloat(10 - 1)
|
||||||
|
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||||
|
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
|
||||||
|
keyframes.append(CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
|
||||||
|
for i in 0 ..< 10 {
|
||||||
|
let k = CGFloat(i) / CGFloat(10 - 1)
|
||||||
|
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||||
|
let y = a * x * x + b * x + c
|
||||||
|
keyframes.append(CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyframes
|
||||||
|
}
|
||||||
|
|
||||||
|
let playIconAnimation: (Double) -> Void = { duration in
|
||||||
|
self.avatarNode.contentNode.alpha = 0.0
|
||||||
|
self.avatarNode.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||||
|
self.avatarNode.contentNode.layer.animateScale(from: 1.0, to: 0.01, duration: duration, removeOnCompletion: false)
|
||||||
|
|
||||||
|
if let view = self.animation.view as? LottieComponent.View {
|
||||||
|
view.alpha = 1.0
|
||||||
|
view.playOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if component.peer.id == component.context.account.peerId {
|
||||||
|
playIconAnimation(0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = self.bounds.height - self.backgroundView.frame.minY
|
||||||
|
self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||||
|
if component.peer.id != component.context.account.peerId {
|
||||||
|
playIconAnimation(0.1)
|
||||||
|
}
|
||||||
|
HapticFeedback().success()
|
||||||
|
})
|
||||||
|
|
||||||
|
if let component = self.component {
|
||||||
|
let fromPoint = self.avatarNode.view.convert(component.sourceFrame.center, from: nil).offsetBy(dx: 0.0, dy: -offset)
|
||||||
|
let positionValues = generateAvatarParabollicMotionKeyframes(from: fromPoint, to: .zero, elevation: 20.0)
|
||||||
|
self.avatarNode.layer.animateKeyframes(values: positionValues.map { NSValue(cgPoint: $0) }, duration: 0.35, keyPath: "position", additive: true)
|
||||||
|
self.avatarNode.layer.animateScale(from: component.sourceFrame.width / self.avatarNode.bounds.width, to: 1.0, duration: 0.35)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.isUpdating {
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
|
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
self.backgroundView.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: QuickShareToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.isUpdating = true
|
||||||
|
defer {
|
||||||
|
self.isUpdating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||||
|
|
||||||
|
if self.component == nil {
|
||||||
|
self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.environment?.controller()?.dismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.environment = environment
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
|
||||||
|
|
||||||
|
let tabBarHeight: CGFloat
|
||||||
|
if !environment.safeInsets.left.isZero {
|
||||||
|
tabBarHeight = 34.0 + environment.safeInsets.bottom
|
||||||
|
} else {
|
||||||
|
tabBarHeight = 49.0 + environment.safeInsets.bottom
|
||||||
|
}
|
||||||
|
let containerInsets = UIEdgeInsets(
|
||||||
|
top: environment.safeInsets.top,
|
||||||
|
left: environment.safeInsets.left + 12.0,
|
||||||
|
bottom: tabBarHeight + 3.0,
|
||||||
|
right: environment.safeInsets.right + 12.0
|
||||||
|
)
|
||||||
|
|
||||||
|
let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom)
|
||||||
|
|
||||||
|
let spacing: CGFloat = 8.0
|
||||||
|
|
||||||
|
let iconSize = CGSize(width: 30.0, height: 30.0)
|
||||||
|
|
||||||
|
let tooltipText: String
|
||||||
|
var overrideImage: AvatarNodeImageOverride?
|
||||||
|
var animationName: String = "anim_forward"
|
||||||
|
if component.peer.id == component.context.account.peerId {
|
||||||
|
tooltipText = environment.strings.Conversation_ForwardTooltip_SavedMessages_One
|
||||||
|
overrideImage = .savedMessagesIcon
|
||||||
|
animationName = "anim_savedmessages"
|
||||||
|
} else {
|
||||||
|
tooltipText = environment.strings.Conversation_ForwardTooltip_Chat_One(component.peer.compactDisplayTitle).string
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentSize = self.content.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(MultilineTextComponent(text: .markdown(
|
||||||
|
text: tooltipText,
|
||||||
|
attributes: MarkdownAttributes(
|
||||||
|
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
|
||||||
|
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)),
|
||||||
|
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
|
||||||
|
linkAttribute: { _ in return nil })
|
||||||
|
))),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - 16.0, height: availableContentSize.height)
|
||||||
|
)
|
||||||
|
|
||||||
|
var contentHeight: CGFloat = 0.0
|
||||||
|
contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height)
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
|
||||||
|
self.avatarNode.setPeer(context: component.context, theme: environment.theme, peer: component.peer, overrideImage: overrideImage, synchronousLoad: true)
|
||||||
|
self.avatarNode.updateSize(size: avatarFrame.size)
|
||||||
|
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
|
||||||
|
|
||||||
|
let _ = self.animation.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(LottieComponent(
|
||||||
|
content: LottieComponent.AppBundleContent(
|
||||||
|
name: animationName
|
||||||
|
),
|
||||||
|
size: CGSize(width: 38.0, height: 38.0),
|
||||||
|
loop: false
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: iconSize
|
||||||
|
)
|
||||||
|
if let animationView = self.animation.view {
|
||||||
|
if animationView.superview == nil {
|
||||||
|
animationView.alpha = 0.0
|
||||||
|
self.avatarNode.view.addSubview(animationView)
|
||||||
|
}
|
||||||
|
animationView.frame = CGRect(origin: .zero, size: iconSize).insetBy(dx: -2.0, dy: -2.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let contentView = self.content.view {
|
||||||
|
if contentView.superview == nil {
|
||||||
|
self.contentView.addSubview(contentView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = CGSize(width: availableContentSize.width, height: contentHeight)
|
||||||
|
let backgroundFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size)
|
||||||
|
|
||||||
|
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
|
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition)
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||||
|
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: backgroundFrame.size))
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class QuickShareToastScreen: ViewControllerComponentContainer {
|
||||||
|
private var processedDidAppear: Bool = false
|
||||||
|
private var processedDidDisappear: Bool = false
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
peer: EnginePeer,
|
||||||
|
sourceFrame: CGRect,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
super.init(
|
||||||
|
context: context,
|
||||||
|
component: QuickShareToastScreenComponent(
|
||||||
|
context: context,
|
||||||
|
peer: peer,
|
||||||
|
sourceFrame: sourceFrame,
|
||||||
|
action: action
|
||||||
|
),
|
||||||
|
navigationBarAppearance: .none,
|
||||||
|
statusBarStyle: .ignore,
|
||||||
|
presentationMode: .default,
|
||||||
|
updatedPresentationData: nil
|
||||||
|
)
|
||||||
|
self.navigationPresentation = .flatModal
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
}
|
||||||
|
|
||||||
|
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if !self.processedDidAppear {
|
||||||
|
self.processedDidAppear = true
|
||||||
|
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
|
||||||
|
componentView.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func superDismiss() {
|
||||||
|
super.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func dismiss(completion: (() -> Void)? = nil) {
|
||||||
|
if !self.processedDidDisappear {
|
||||||
|
self.processedDidDisappear = true
|
||||||
|
|
||||||
|
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
|
||||||
|
componentView.animateOut(completion: { [weak self] in
|
||||||
|
if let self {
|
||||||
|
self.superDismiss()
|
||||||
|
}
|
||||||
|
completion?()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
super.dismiss(completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -280,7 +280,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||||||
public let attemptedNavigationToPrivateQuote: (Peer?) -> Void
|
public let 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,
|
||||||
|
@ -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: {},
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct QuadVertexOut {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
constant static float2 quadVertices[6] = {
|
||||||
|
float2(0.0, 0.0),
|
||||||
|
float2(1.0, 0.0),
|
||||||
|
float2(0.0, 1.0),
|
||||||
|
float2(1.0, 0.0),
|
||||||
|
float2(0.0, 1.0),
|
||||||
|
float2(1.0, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex QuadVertexOut cameraBlobVertex(
|
||||||
|
constant float4 &rect [[ buffer(0) ]],
|
||||||
|
uint vid [[ vertex_id ]]
|
||||||
|
) {
|
||||||
|
float2 quadVertex = quadVertices[vid];
|
||||||
|
|
||||||
|
QuadVertexOut out;
|
||||||
|
out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0);
|
||||||
|
out.position.x = -1.0 + out.position.x * 2.0;
|
||||||
|
out.position.y = -1.0 + out.position.y * 2.0;
|
||||||
|
|
||||||
|
out.uv = quadVertex;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define BindingDistance 0.45
|
||||||
|
#define AARadius 2.0
|
||||||
|
#define PersistenceFactor 1.1
|
||||||
|
|
||||||
|
float smin(float a, float b, float k) {
|
||||||
|
float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
|
||||||
|
h = pow(h, 0.6);
|
||||||
|
return mix(a, b, h) - k * h * (1.0 - h) * 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdfCircle(float2 uv, float2 position, float radius) {
|
||||||
|
return length(uv - position) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dynamicBinding(float2 primaryPos, float2 secondaryPos) {
|
||||||
|
float distance = length(primaryPos - secondaryPos);
|
||||||
|
return BindingDistance * (1.0 + PersistenceFactor * smoothstep(0.0, 2.0, distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
float map(float2 uv, float2 primaryParameters, float2 primaryOffset, float2 secondaryParameters, float2 secondaryOffset) {
|
||||||
|
float primary = sdfCircle(uv, primaryOffset, primaryParameters.x);
|
||||||
|
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
|
||||||
|
|
||||||
|
float bindDist = dynamicBinding(primaryOffset, secondaryOffset);
|
||||||
|
|
||||||
|
float metaballs = 1.0;
|
||||||
|
metaballs = smin(metaballs, primary, bindDist);
|
||||||
|
metaballs = smin(metaballs, secondary, bindDist);
|
||||||
|
return metaballs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment half4 cameraBlobFragment(
|
||||||
|
QuadVertexOut in [[stage_in]],
|
||||||
|
constant float2 &primaryParameters [[buffer(0)]],
|
||||||
|
constant float2 &primaryOffset [[buffer(1)]],
|
||||||
|
constant float2 &secondaryParameters [[buffer(2)]],
|
||||||
|
constant float2 &secondaryOffset [[buffer(3)]],
|
||||||
|
constant float2 &resolution [[buffer(4)]]
|
||||||
|
) {
|
||||||
|
float aspectRatio = resolution.x / resolution.y;
|
||||||
|
float2 uv = in.uv * 2.0 - 1.0;
|
||||||
|
uv.x *= aspectRatio;
|
||||||
|
|
||||||
|
float t = AARadius / resolution.y;
|
||||||
|
|
||||||
|
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
|
||||||
|
|
||||||
|
if (primaryParameters.y > 0) {
|
||||||
|
float innerHoleRadius = primaryParameters.y;
|
||||||
|
float hole = smoothstep(-t, t, length(uv - primaryOffset) - innerHoleRadius);
|
||||||
|
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
|
||||||
|
|
||||||
|
c *= mix(1.0, hole, primaryInfluence);
|
||||||
|
} else if (primaryParameters.y < 0) {
|
||||||
|
float cutRadius = abs(primaryParameters.y);
|
||||||
|
float2 primaryFeatheredOffset = primaryOffset;
|
||||||
|
primaryFeatheredOffset.x *= 1.129;
|
||||||
|
|
||||||
|
float distFromCenter = length(uv - primaryFeatheredOffset);
|
||||||
|
|
||||||
|
float gradientWidth = 0.21;
|
||||||
|
float featheredEdge = smoothstep(cutRadius - gradientWidth, cutRadius + gradientWidth, distFromCenter);
|
||||||
|
|
||||||
|
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
|
||||||
|
|
||||||
|
float horizontalFade = 1.0;
|
||||||
|
float rightEdgePosition = 0.94 * aspectRatio;
|
||||||
|
if (uv.x > rightEdgePosition) {
|
||||||
|
horizontalFade = 1.0 - smoothstep(0.0, 0.22, uv.x - rightEdgePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
c *= mix(1.0, featheredEdge, primaryInfluence) * horizontalFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
return half4(0.0, 0.0, 0.0, c);
|
||||||
|
}
|
@ -0,0 +1,545 @@
|
|||||||
|
import Foundation
|
||||||
|
import Display
|
||||||
|
import Metal
|
||||||
|
import MetalKit
|
||||||
|
import MetalEngine
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
|
||||||
|
private var metalLibraryValue: MTLLibrary?
|
||||||
|
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
|
||||||
|
if let metalLibraryValue {
|
||||||
|
return metalLibraryValue
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainBundle = Bundle(for: StoryBlobLayer.self)
|
||||||
|
guard let path = mainBundle.path(forResource: "StoryPeerListBundle", ofType: "bundle") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let bundle = Bundle(path: path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metalLibraryValue = library
|
||||||
|
return library
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PropertyAnimation<T: Interpolatable> {
|
||||||
|
let from: T
|
||||||
|
let to: T
|
||||||
|
let animation: ComponentTransition.Animation
|
||||||
|
let startTimestamp: Double
|
||||||
|
private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable
|
||||||
|
|
||||||
|
init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) {
|
||||||
|
self.from = fromValue
|
||||||
|
self.to = toValue
|
||||||
|
self.animation = animation
|
||||||
|
self.startTimestamp = startTimestamp
|
||||||
|
self.interpolator = T.interpolator()
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueAt(_ t: CGFloat) -> Interpolatable {
|
||||||
|
if t <= 0.0 {
|
||||||
|
return self.from
|
||||||
|
} else if t >= 1.0 {
|
||||||
|
return self.to
|
||||||
|
} else {
|
||||||
|
return self.interpolator(self.from, self.to, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class AnimatableProperty<T: Interpolatable> {
|
||||||
|
var presentationValue: T
|
||||||
|
var value: T
|
||||||
|
private var animation: PropertyAnimation<T>?
|
||||||
|
|
||||||
|
init(value: T) {
|
||||||
|
self.value = value
|
||||||
|
self.presentationValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(value: T, transition: ComponentTransition = .immediate) {
|
||||||
|
let currentTimestamp = CACurrentMediaTime()
|
||||||
|
if case .none = transition.animation {
|
||||||
|
if let animation = self.animation, case let .curve(duration, curve) = animation.animation {
|
||||||
|
self.value = value
|
||||||
|
let elapsed = duration - (currentTimestamp - animation.startTimestamp)
|
||||||
|
if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 {
|
||||||
|
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp)
|
||||||
|
} else {
|
||||||
|
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.value = value
|
||||||
|
self.presentationValue = value
|
||||||
|
self.animation = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.value = value
|
||||||
|
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tick(timestamp: Double) -> Bool {
|
||||||
|
guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeFromStart = timestamp - animation.startTimestamp
|
||||||
|
var t = max(0.0, timeFromStart / duration)
|
||||||
|
switch curve {
|
||||||
|
case .linear:
|
||||||
|
break
|
||||||
|
case .easeInOut:
|
||||||
|
t = listViewAnimationCurveEaseInOut(t)
|
||||||
|
case .spring:
|
||||||
|
t = lookupSpringValue(t)
|
||||||
|
case let .custom(x1, y1, x2, y2):
|
||||||
|
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
|
||||||
|
}
|
||||||
|
self.presentationValue = animation.valueAt(t) as! T
|
||||||
|
|
||||||
|
if timeFromStart <= duration {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
self.animation = nil
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
|
||||||
|
let table: [(CGFloat, CGFloat)] = [
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0625, 0.1123005598783493),
|
||||||
|
(0.125, 0.31598418951034546),
|
||||||
|
(0.1875, 0.5103585720062256),
|
||||||
|
(0.25, 0.6650152802467346),
|
||||||
|
(0.3125, 0.777747631072998),
|
||||||
|
(0.375, 0.8557760119438171),
|
||||||
|
(0.4375, 0.9079672694206238),
|
||||||
|
(0.5, 0.942038357257843),
|
||||||
|
(0.5625, 0.9638798832893372),
|
||||||
|
(0.625, 0.9776856303215027),
|
||||||
|
(0.6875, 0.9863143563270569),
|
||||||
|
(0.75, 0.991658091545105),
|
||||||
|
(0.8125, 0.9949421286582947),
|
||||||
|
(0.875, 0.9969474077224731),
|
||||||
|
(0.9375, 0.9981651306152344),
|
||||||
|
(1.0, 1.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in 0 ..< table.count - 2 {
|
||||||
|
let lhs = table[i]
|
||||||
|
let rhs = table[i + 1]
|
||||||
|
|
||||||
|
if t >= lhs.0 && t <= rhs.0 {
|
||||||
|
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
|
||||||
|
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StoryBlobLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||||
|
var internalData: MetalEngineSubjectInternalData?
|
||||||
|
|
||||||
|
private final class RenderState: RenderToLayerState {
|
||||||
|
let pipelineState: MTLRenderPipelineState
|
||||||
|
|
||||||
|
required init?(device: MTLDevice) {
|
||||||
|
guard let library = metalLibrary(device: device) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let vertexFunction = library.makeFunction(name: "cameraBlobVertex"),
|
||||||
|
let fragmentFunction = library.makeFunction(name: "cameraBlobFragment") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = vertexFunction
|
||||||
|
pipelineDescriptor.fragmentFunction = fragmentFunction
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
|
||||||
|
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
|
||||||
|
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
|
||||||
|
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
|
||||||
|
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
|
||||||
|
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.pipelineState = pipelineState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var primarySize = AnimatableProperty<CGFloat>(value: 1.0)
|
||||||
|
private var primaryHoleSize = AnimatableProperty<CGFloat>(value: 0.0)
|
||||||
|
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
|
||||||
|
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
|
||||||
|
|
||||||
|
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.85)
|
||||||
|
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
|
||||||
|
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
|
||||||
|
|
||||||
|
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
|
||||||
|
private var hasActiveAnimations: Bool = false
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.isOpaque = false
|
||||||
|
|
||||||
|
self.didEnterHierarchy = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateAnimations()
|
||||||
|
if self.hasActiveAnimations {
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.didExitHierarchy = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.displayLinkSubscription = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(layer: Any) {
|
||||||
|
super.init(layer: layer)
|
||||||
|
|
||||||
|
if let layer = layer as? StoryBlobLayer {
|
||||||
|
self.primarySize = layer.primarySize
|
||||||
|
self.primaryHoleSize = layer.primaryHoleSize
|
||||||
|
self.primaryOffsetX = layer.primaryOffsetX
|
||||||
|
self.primaryOffsetY = layer.primaryOffsetY
|
||||||
|
self.secondarySize = layer.secondarySize
|
||||||
|
self.secondaryOffsetX = layer.secondaryOffsetX
|
||||||
|
self.secondaryOffsetY = layer.secondaryOffsetY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePrimarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.height > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedSize = size / self.bounds.height
|
||||||
|
self.primarySize.update(value: mappedSize, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePrimaryHoleSize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.height > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedSize = size / self.bounds.height
|
||||||
|
self.primaryHoleSize.update(value: mappedSize, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.height > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedOffset = offset / self.bounds.height * 2.0
|
||||||
|
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.width > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedOffset = offset / self.bounds.width * 2.0
|
||||||
|
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSecondarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.height > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedSize = size / self.bounds.height
|
||||||
|
self.secondarySize.update(value: mappedSize, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.height > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedOffset = offset / self.bounds.height * 2.0
|
||||||
|
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
|
||||||
|
guard self.bounds.width > 0.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mappedOffset = offset / self.bounds.width * 2.0
|
||||||
|
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
|
||||||
|
|
||||||
|
self.setNeedsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAnimations() {
|
||||||
|
let properties = [
|
||||||
|
self.primarySize,
|
||||||
|
self.primaryHoleSize,
|
||||||
|
self.primaryOffsetX,
|
||||||
|
self.primaryOffsetY,
|
||||||
|
self.secondarySize,
|
||||||
|
self.secondaryOffsetX,
|
||||||
|
self.secondaryOffsetY,
|
||||||
|
]
|
||||||
|
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
var hasAnimations = false
|
||||||
|
for property in properties {
|
||||||
|
if property.tick(timestamp: timestamp) {
|
||||||
|
hasAnimations = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.hasActiveAnimations = hasAnimations
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(context: MetalEngineSubjectContext) {
|
||||||
|
if self.bounds.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
|
||||||
|
|
||||||
|
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, commands: { encoder, placement in
|
||||||
|
let effectiveRect = placement.effectiveRect
|
||||||
|
|
||||||
|
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
|
||||||
|
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
||||||
|
|
||||||
|
var primaryParameters = simd_float2(
|
||||||
|
Float(self.primarySize.presentationValue),
|
||||||
|
Float(self.primaryHoleSize.presentationValue)
|
||||||
|
)
|
||||||
|
encoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float2>.size, index: 0)
|
||||||
|
|
||||||
|
var primaryOffset = simd_float2(
|
||||||
|
Float(self.primaryOffsetX.presentationValue),
|
||||||
|
Float(self.primaryOffsetY.presentationValue)
|
||||||
|
)
|
||||||
|
encoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 1)
|
||||||
|
|
||||||
|
var secondaryParameters = simd_float2(
|
||||||
|
Float(self.secondarySize.presentationValue),
|
||||||
|
Float(0)
|
||||||
|
)
|
||||||
|
encoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float2>.size, index: 2)
|
||||||
|
|
||||||
|
var secondaryOffset = simd_float2(
|
||||||
|
Float(self.secondaryOffsetX.presentationValue),
|
||||||
|
Float(self.secondaryOffsetY.presentationValue)
|
||||||
|
)
|
||||||
|
encoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 3)
|
||||||
|
|
||||||
|
var resolution = simd_float2(
|
||||||
|
Float(drawableSize.width),
|
||||||
|
Float(drawableSize.height)
|
||||||
|
)
|
||||||
|
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_float2>.size, index: 4)
|
||||||
|
|
||||||
|
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StoryComposeLayer: CALayer {
|
||||||
|
private let theme: PresentationTheme?
|
||||||
|
private let strings: PresentationStrings?
|
||||||
|
|
||||||
|
private let blobLayer = SimpleLayer()
|
||||||
|
private let maskLayer = StoryBlobLayer()
|
||||||
|
private let backgroundLayer = SimpleGradientLayer()
|
||||||
|
private let foregroundLayer = SimpleGradientLayer()
|
||||||
|
private let iconLayer = SimpleLayer()
|
||||||
|
private let labelLayer = SimpleLayer()
|
||||||
|
|
||||||
|
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSublayer(self.blobLayer)
|
||||||
|
|
||||||
|
self.blobLayer.mask = self.maskLayer
|
||||||
|
self.blobLayer.masksToBounds = true
|
||||||
|
|
||||||
|
self.backgroundLayer.type = .axial
|
||||||
|
self.backgroundLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
|
||||||
|
self.backgroundLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
|
||||||
|
self.blobLayer.addSublayer(self.backgroundLayer)
|
||||||
|
|
||||||
|
self.foregroundLayer.type = .axial
|
||||||
|
self.foregroundLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
|
||||||
|
self.foregroundLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
|
||||||
|
self.blobLayer.addSublayer(self.foregroundLayer)
|
||||||
|
|
||||||
|
self.iconLayer.contents = generateAddIcon(color: theme.list.itemCheckColors.foregroundColor)?.cgImage
|
||||||
|
self.iconLayer.opacity = 0.0
|
||||||
|
self.blobLayer.addSublayer(self.iconLayer)
|
||||||
|
|
||||||
|
if let image = generateAddLabel(strings: strings, color: theme.list.itemPrimaryTextColor) {
|
||||||
|
self.labelLayer.contents = image.cgImage
|
||||||
|
self.labelLayer.bounds = CGRect(origin: .zero, size: image.size)
|
||||||
|
self.addSublayer(self.labelLayer)
|
||||||
|
self.labelLayer.opacity = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if let blurEffect = CALayer.blur() {
|
||||||
|
self.iconLayer.filters = [blurEffect]
|
||||||
|
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
|
||||||
|
self.labelLayer.filters = [blurEffect]
|
||||||
|
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(layer: Any) {
|
||||||
|
if let layer = layer as? StoryComposeLayer {
|
||||||
|
self.theme = layer.theme
|
||||||
|
self.strings = layer.strings
|
||||||
|
} else {
|
||||||
|
self.theme = nil
|
||||||
|
self.strings = nil
|
||||||
|
}
|
||||||
|
super.init(layer: layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateOffset(_ offset: CGFloat, baseSize: CGFloat, colors: [CGColor], transition: ComponentTransition = .immediate) {
|
||||||
|
self.backgroundLayer.colors = colors
|
||||||
|
self.foregroundLayer.locations = [0.0, 0.68, 1.0]
|
||||||
|
|
||||||
|
if let theme = self.theme {
|
||||||
|
let fillColor = theme.list.itemCheckColors.fillColor
|
||||||
|
self.foregroundLayer.colors = [
|
||||||
|
fillColor.cgColor,
|
||||||
|
fillColor.withAlphaComponent(0.0).cgColor,
|
||||||
|
fillColor.withAlphaComponent(0.0).cgColor
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
var holeSize = baseSize > 52.0 ? 58.0 : -60.0
|
||||||
|
var primaryBaseScale = 1.0
|
||||||
|
var secondaryBaseScale = 0.68
|
||||||
|
if holeSize < 0.0 {
|
||||||
|
if offset > 48.0 {
|
||||||
|
holeSize -= min(14.0, (max(0.0, (offset - 48.0)) / 20.0) * 14.0)
|
||||||
|
}
|
||||||
|
primaryBaseScale = 0.96 + min(1.0, max(0.0, (offset - 20.0)) / 10.0) * 0.04
|
||||||
|
secondaryBaseScale = 0.62
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondaryScale = secondaryBaseScale
|
||||||
|
if offset < 20.0 {
|
||||||
|
secondaryScale += (offset / 20.0) * 0.2
|
||||||
|
} else {
|
||||||
|
secondaryScale = min(1.0, max(secondaryBaseScale + 0.2, offset / 75.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.maskLayer.updatePrimaryOffsetX(85.0, transition: transition)
|
||||||
|
self.maskLayer.updatePrimarySize(baseSize * primaryBaseScale, transition: transition)
|
||||||
|
self.maskLayer.updateSecondaryOffsetX(85.0 - offset, transition: transition)
|
||||||
|
self.maskLayer.updatePrimaryHoleSize(holeSize, transition: transition)
|
||||||
|
self.maskLayer.updateSecondarySize(55.0 * secondaryScale, transition: transition)
|
||||||
|
|
||||||
|
if holeSize < 0.0 {
|
||||||
|
transition.setAlpha(layer: self.blobLayer, alpha: min(1.0, max(0.0, offset / 10.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
let layerX = self.bounds.width - offset + 5.0
|
||||||
|
transition.setPosition(layer: self.iconLayer, position: CGPoint(x: layerX, y: self.bounds.height / 2.0))
|
||||||
|
transition.setPosition(layer: self.labelLayer, position: CGPoint(x: layerX, y: self.bounds.height + 13.0 - UIScreenPixel))
|
||||||
|
|
||||||
|
let iconOffset = max(0.0, offset - 35.0)
|
||||||
|
let alpha = max(0.0, min(1.0, iconOffset / 15.0))
|
||||||
|
let blurRadius = 8.0 - min(8.0, (iconOffset / 20.0) * 8.0)
|
||||||
|
|
||||||
|
for layer in [self.iconLayer, self.labelLayer] {
|
||||||
|
layer.setValue(blurRadius as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius")
|
||||||
|
transition.setAlpha(layer: layer, alpha: alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSublayers() {
|
||||||
|
super.layoutSublayers()
|
||||||
|
|
||||||
|
self.blobLayer.frame = self.bounds
|
||||||
|
self.maskLayer.frame = self.bounds
|
||||||
|
self.backgroundLayer.frame = self.bounds
|
||||||
|
self.foregroundLayer.frame = self.bounds
|
||||||
|
self.iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateAddIcon(color: UIColor) -> UIImage? {
|
||||||
|
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||||
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
|
context.setStrokeColor(color.cgColor)
|
||||||
|
context.setLineWidth(3.0)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: 15.0, y: 5.5))
|
||||||
|
context.addLine(to: CGPoint(x: 15.0, y: 24.5))
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: 5.5, y: 15.0))
|
||||||
|
context.addLine(to: CGPoint(x: 24.5, y: 15.0))
|
||||||
|
context.strokePath()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? {
|
||||||
|
let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
|
||||||
|
var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
|
||||||
|
textRect.size.width = ceil(textRect.size.width)
|
||||||
|
textRect.size.height = ceil(textRect.size.height)
|
||||||
|
|
||||||
|
return generateImage(textRect.size, rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
titleString.draw(in: textRect)
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
})
|
||||||
|
}
|
@ -60,6 +60,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
public let 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
|
||||||
)),
|
)),
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,7 +418,7 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.dismissPreviewing?()
|
let _ = strongSelf.dismissPreviewing?(false)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
case .replyThread:
|
case .replyThread:
|
||||||
|
@ -292,11 +292,20 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keepDefaultContentTouches = false
|
||||||
|
for media in message.media {
|
||||||
|
if media is TelegramMediaImage {
|
||||||
|
keepDefaultContentTouches = true
|
||||||
|
} else if let file = media as? TelegramMediaFile, file.isVideo {
|
||||||
|
keepDefaultContentTouches = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let source: ContextContentSource
|
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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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? {
|
||||||
|
@ -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()
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user