Various improvements

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1502,7 +1502,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.presentInGlobalOverlay(contextController)
}
} else {
var dismissPreviewingImpl: (() -> Void)?
var dismissPreviewingImpl: ((Bool) -> (() -> Void))?
let source: ContextContentSource
if let location = location {
source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location))
@ -1510,8 +1510,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
chatController.customNavigationController = strongSelf.navigationController as? NavigationController
chatController.canReadHistory.set(false)
chatController.dismissPreviewing = {
dismissPreviewingImpl?()
chatController.dismissPreviewing = { animateIn in
return dismissPreviewingImpl?(animateIn) ?? {}
}
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
@ -1519,8 +1519,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
dismissPreviewingImpl = { [weak contextController] in
contextController?.dismiss()
dismissPreviewingImpl = { [weak self, weak contextController] animateIn in
if let self, let contextController {
if animateIn {
contextController.statusBar.statusBarStyle = .Ignore
self.present(contextController, in: .window(.root))
return {
contextController.dismissNow()
}
} else {
contextController.dismiss()
}
}
return {}
}
}
case let .forum(pinnedIndex, _, threadId, _, _):
@ -2794,7 +2805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
private weak var storyCameraTooltip: TooltipScreen?
fileprivate func openStoryCamera(fromList: Bool) {
fileprivate func openStoryCamera(fromList: Bool, gesturePullOffset: CGFloat? = nil) {
guard !self.context.isFrozen else {
let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)
self.push(controller)
@ -2920,8 +2931,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
cameraTransitionIn = StoryCameraTransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5
sourceRect: gesturePullOffset.flatMap({ transitionView.bounds.offsetBy(dx: -$0, dy: 0) }) ?? transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5,
useFillAnimation: gesturePullOffset != nil
)
}
} else {
@ -2929,7 +2941,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
cameraTransitionIn = StoryCameraTransitionIn(
sourceView: rightButtonView,
sourceRect: rightButtonView.bounds,
sourceCornerRadius: rightButtonView.bounds.height * 0.5
sourceCornerRadius: rightButtonView.bounds.height * 0.5,
useFillAnimation: false
)
}
}
@ -2983,6 +2996,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if case .chatList = self.location, let componentView = self.chatListHeaderView() {
componentView.storyComposeAction = { [weak self] offset in
guard let self else {
return
}
self.openStoryCamera(fromList: true, gesturePullOffset: offset)
}
componentView.storyPeerAction = { [weak self] peer in
guard let self else {
return

View File

@ -1748,7 +1748,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
if peer.smallProfileImage != nil && overrideImage == nil {
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: avatarDiameter, height: avatarDiameter))
} else {
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1668,15 +1668,18 @@ public class CameraScreenImpl: ViewController, CameraScreen {
public weak var sourceView: UIView?
public let sourceRect: CGRect
public let sourceCornerRadius: CGFloat
public let useFillAnimation: Bool
public init(
sourceView: UIView,
sourceRect: CGRect,
sourceCornerRadius: CGFloat
sourceCornerRadius: CGFloat,
useFillAnimation: Bool
) {
self.sourceView = sourceView
self.sourceRect = sourceRect
self.sourceCornerRadius = sourceCornerRadius
self.useFillAnimation = useFillAnimation
}
}
@ -2505,55 +2508,100 @@ public class CameraScreenImpl: ViewController, CameraScreen {
if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
if case .story = controller.mode {
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
if transitionIn.useFillAnimation {
self.backgroundView.alpha = 1.0
self.backgroundView.layer.removeAllAnimations()
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.transitionDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.previewContainerView.layer.animate(
from: minSide / 2.0 as NSNumber,
to: self.previewContainerView.layer.cornerRadius as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3
)
} else {
self.mainPreviewAnimationWrapperView.bounds = self.mainPreviewView.bounds
self.mainPreviewAnimationWrapperView.center = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
let transitionMaskView = UIView()
transitionMaskView.frame = self.view.bounds
self.view.mask = transitionMaskView
self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
let transitionCircleLayer = SimpleShapeLayer()
transitionCircleLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 320.0, height: 320.0)), transform: nil)
transitionCircleLayer.fillColor = UIColor.white.cgColor
transitionCircleLayer.frame = CGSize(width: 320.0, height: 320.0).centered(in: sourceLocalFrame)
transitionMaskView.layer.addSublayer(transitionCircleLayer)
let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView)
let sourceCenter = sourceInnerFrame.center
self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
let colorFillView = UIView()
colorFillView.backgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor
colorFillView.frame = self.view.bounds
colorFillView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.view.addSubview(colorFillView)
var sourceBounds = self.mainPreviewView.bounds
if let holder = controller.holder {
sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size))
let iconLayer = SimpleLayer()
iconLayer.contents = generateAddIcon(color: self.presentationData.theme.list.itemCheckColors.foregroundColor)?.cgImage
iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
iconLayer.position = sourceLocalFrame.center
colorFillView.layer.addSublayer(iconLayer)
let labelLayer = SimpleLayer()
if let image = generateAddLabel(strings: self.presentationData.strings, color: self.presentationData.theme.list.itemCheckColors.foregroundColor) {
labelLayer.contents = image.cgImage
labelLayer.bounds = CGRect(origin: .zero, size: image.size)
labelLayer.position = CGPoint(x: sourceLocalFrame.center.x, y: sourceLocalFrame.center.y + 43.0 - UIScreenPixel)
colorFillView.layer.addSublayer(labelLayer)
}
self.mainPreviewAnimationWrapperView.layer.animateBounds(from: sourceBounds, to: self.mainPreviewView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let sourceScale = max(sourceInnerFrame.width / self.previewContainerView.frame.width, sourceInnerFrame.height / self.previewContainerView.frame.height)
self.mainPreviewView.transform = CGAffineTransform.identity
self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.mainPreviewContainerView.addSubview(self.mainPreviewView)
Queue.mainQueue().justDispatch {
self.animatedIn = true
}
iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false)
labelLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false)
transitionCircleLayer.animateScale(from: sourceLocalFrame.width / 320.0, to: 6.0, duration: 0.6, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
self.view.mask = nil
colorFillView.removeFromSuperview()
})
}
if let view = self.componentHost.view {
view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
if case .story = controller.mode {
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.previewContainerView.layer.animate(
from: minSide / 2.0 as NSNumber,
to: self.previewContainerView.layer.cornerRadius as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3
)
} else {
self.mainPreviewAnimationWrapperView.bounds = self.mainPreviewView.bounds
self.mainPreviewAnimationWrapperView.center = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView)
let sourceCenter = sourceInnerFrame.center
self.mainPreviewAnimationWrapperView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
var sourceBounds = self.mainPreviewView.bounds
if let holder = controller.holder {
sourceBounds = CGRect(origin: .zero, size: holder.parentView.frame.size.aspectFitted(sourceBounds.size))
}
self.mainPreviewAnimationWrapperView.layer.animateBounds(from: sourceBounds, to: self.mainPreviewView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let sourceScale = max(sourceInnerFrame.width / self.previewContainerView.frame.width, sourceInnerFrame.height / self.previewContainerView.frame.height)
self.mainPreviewView.transform = CGAffineTransform.identity
self.mainPreviewAnimationWrapperView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.mainPreviewContainerView.addSubview(self.mainPreviewView)
Queue.mainQueue().justDispatch {
self.animatedIn = true
}
})
}
if let view = self.componentHost.view {
view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
}
@ -3767,6 +3815,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.dismiss(animated: false)
}
}
public func animateIn() {
self.node.animateIn()
}
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
if let layout = self.validLayout, layout.metrics.isTablet {
@ -4009,3 +4061,37 @@ private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoi
return position
}
private func generateAddIcon(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(3.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 15.0, y: 5.5))
context.addLine(to: CGPoint(x: 15.0, y: 24.5))
context.strokePath()
context.move(to: CGPoint(x: 5.5, y: 15.0))
context.addLine(to: CGPoint(x: 24.5, y: 15.0))
context.strokePath()
})
}
private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? {
let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
textRect.size.width = ceil(textRect.size.width)
textRect.size.height = ceil(textRect.size.height)
return generateImage(textRect.size, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
UIGraphicsPushContext(context)
titleString.draw(in: textRect)
UIGraphicsPopContext()
})
}

View File

@ -1480,6 +1480,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
updatedShareButtonNode.pressed = { [weak strongSelf] in
strongSelf?.shareButtonPressed()
}
updatedShareButtonNode.longPressAction = { [weak strongSelf] node, gesture in
strongSelf?.openQuickShare(node: node, gesture: gesture)
}
}
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account)
updatedShareButtonNode.frame = CGRect(origin: CGPoint(x: !incoming ? updatedImageFrame.minX - buttonSize.width - 6.0 : updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - buttonSize.height - 4.0 + imageBottomPadding), size: buttonSize)
@ -2429,6 +2432,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
if let item = self.item {
item.controllerInteraction.displayQuickShare(item.message.id, node, gesture)
}
}
private var playedSwipeToReplyHaptic = false
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0

View File

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

View File

@ -1012,6 +1012,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
updatedShareButtonNode.pressed = { [weak strongSelf] in
strongSelf?.shareButtonPressed()
}
updatedShareButtonNode.longPressAction = { [weak strongSelf] node, gesture in
strongSelf?.openQuickShare(node: node, gesture: gesture)
}
}
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account)
let shareButtonFrame = CGRect(origin: CGPoint(x: baseShareButtonFrame.minX, y: baseShareButtonFrame.maxY - buttonSize.height), size: buttonSize)
@ -1546,6 +1549,12 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
private func openQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
if let item = self.item {
item.controllerInteraction.displayQuickShare(item.message.id, node, gesture)
}
}
private var playedSwipeToReplyHaptic = false
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0

View File

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

View File

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

View File

@ -289,7 +289,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
backgroundSize.height += verticalInset
let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += titleLayout.size.height
backgroundSize.height += verticalSpacing
@ -297,7 +297,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
backgroundSize.height += subtitleLayout.size.height
backgroundSize.height += verticalSpacing + paragraphSpacing
let infoConstrainedSize = CGSize(width: constrainedWidth * 0.7, height: CGFloat.greatestFiniteMagnitude)
let infoConstrainedSize = CGSize(width: floor(constrainedWidth * 0.7), height: CGFloat.greatestFiniteMagnitude)
var maxTitleWidth: CGFloat = 0.0
var maxValueWidth: CGFloat = 0.0
@ -389,7 +389,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
groupsValueLayoutAndApply = nil
}
backgroundSize.width = horizontalContentInset * 2.0 + maxTitleWidth + attributeSpacing + maxValueWidth
backgroundSize.width = horizontalContentInset * 2.0 + max(titleLayout.size.width, maxTitleWidth + attributeSpacing + maxValueWidth)
let disclaimerText: NSMutableAttributedString
if let verification = item.verification {

View File

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

View File

@ -8,27 +8,38 @@ import TelegramCore
import TextFormat
import TelegramPresentationData
import MultilineTextComponent
import LottieComponent
import AccountContext
import ViewControllerComponent
import AvatarNode
import ComponentDisplayAdapters
private let largeCircleSize: CGFloat = 16.0
private let smallCircleSize: CGFloat = 8.0
private final class QuickShareScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let sourceNode: ASDisplayNode
let gesture: ContextGesture
let openPeer: (EnginePeer.Id) -> Void
let completion: (EnginePeer.Id) -> Void
let ready: Promise<Bool>
init(
context: AccountContext,
sourceNode: ASDisplayNode,
gesture: ContextGesture
gesture: ContextGesture,
openPeer: @escaping (EnginePeer.Id) -> Void,
completion: @escaping (EnginePeer.Id) -> Void,
ready: Promise<Bool>
) {
self.context = context
self.sourceNode = sourceNode
self.gesture = gesture
self.openPeer = openPeer
self.completion = completion
self.ready = ready
}
static func ==(lhs: QuickShareScreenComponent, rhs: QuickShareScreenComponent) -> Bool {
@ -36,10 +47,16 @@ private final class QuickShareScreenComponent: Component {
}
final class View: UIView {
private let backgroundShadowLayer: SimpleLayer
private let backgroundView: BlurredBackgroundView
private let backgroundTintView: UIView
private let containerView: UIView
private let largeCircleLayer: SimpleLayer
private let largeCircleShadowLayer: SimpleLayer
private let smallCircleLayer: SimpleLayer
private let smallCircleShadowLayer: SimpleLayer
private var items: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var isUpdating: Bool = false
@ -55,12 +72,30 @@ private final class QuickShareScreenComponent: Component {
private var initialContinueGesturePoint: CGPoint?
private var didMoveFromInitialGesturePoint = false
private let hapticFeedback = HapticFeedback()
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundView.clipsToBounds = true
self.backgroundTintView = UIView()
self.backgroundTintView.clipsToBounds = true
self.backgroundShadowLayer = SimpleLayer()
self.backgroundShadowLayer.opacity = 0.0
self.largeCircleLayer = SimpleLayer()
self.largeCircleShadowLayer = SimpleLayer()
self.smallCircleLayer = SimpleLayer()
self.smallCircleShadowLayer = SimpleLayer()
self.largeCircleLayer.backgroundColor = UIColor.black.cgColor
self.largeCircleLayer.masksToBounds = true
self.largeCircleLayer.cornerRadius = largeCircleSize / 2.0
self.smallCircleLayer.backgroundColor = UIColor.black.cgColor
self.smallCircleLayer.masksToBounds = true
self.smallCircleLayer.cornerRadius = smallCircleSize / 2.0
self.containerView = UIView()
self.containerView.clipsToBounds = true
@ -68,6 +103,7 @@ private final class QuickShareScreenComponent: Component {
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.backgroundTintView)
self.layer.addSublayer(self.backgroundShadowLayer)
self.addSubview(self.containerView)
}
@ -80,9 +116,40 @@ private final class QuickShareScreenComponent: Component {
}
func animateIn() {
self.hapticFeedback.impact()
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))
transition.animateBoundsSize(view: self.backgroundView, from: CGSize(width: 0.0, height: self.backgroundView.bounds.height), to: self.backgroundView.bounds.size)
transition.animateBounds(view: self.containerView, from: CGRect(x: self.containerView.bounds.width / 2.0, y: 0.0, width: 0.0, height: self.backgroundView.bounds.height), to: self.containerView.bounds)
self.backgroundView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1)
self.backgroundTintView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundTintView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1)
self.backgroundShadowLayer.opacity = 1.0
transition.animateBoundsSize(layer: self.backgroundShadowLayer, from: CGSize(width: 0.0, height: self.backgroundShadowLayer.bounds.height), to: self.backgroundShadowLayer.bounds.size)
self.backgroundShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let mainCircleDelay: Double = 0.01
let backgroundCenter = self.backgroundView.frame.width / 2.0
let backgroundWidth = self.backgroundView.frame.width
for item in self.items.values {
guard let itemView = item.view else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundCenter)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.3
itemView.isHidden = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemView] in
guard let itemView else {
return
}
itemView.isHidden = false
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 0.63 as NSNumber, keyPath: "transform.scale", duration: 0.4)
})
}
Queue.mainQueue().after(0.3) {
self.containerView.clipsToBounds = false
@ -98,21 +165,45 @@ private final class QuickShareScreenComponent: Component {
}
func highlightGestureMoved(location: CGPoint) {
var selectedPeerId: EnginePeer.Id?
for (peerId, view) in self.items {
guard let view = view.view else {
continue
}
if view.frame.contains(location) {
self.selectedPeerId = peerId
self.state?.updated(transition: .spring(duration: 0.3))
if view.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) {
selectedPeerId = peerId
break
}
}
if let selectedPeerId, selectedPeerId != self.selectedPeerId {
self.hapticFeedback.tap()
}
self.selectedPeerId = selectedPeerId
self.state?.updated(transition: .spring(duration: 0.3))
}
func highlightGestureFinished(performAction: Bool) {
if let selectedPeerId = self.selectedPeerId, performAction {
let _ = selectedPeerId
if let component = self.component, let peer = self.peers?.first(where: { $0.id == selectedPeerId }), let view = self.items[selectedPeerId]?.view as? ItemComponent.View, let controller = self.environment?.controller() {
controller.window?.forEachController({ controller in
if let controller = controller as? QuickShareToastScreen {
controller.dismiss()
}
})
let toastScreen = QuickShareToastScreen(
context: component.context,
peer: peer,
sourceFrame: view.convert(view.bounds, to: nil),
action: {
component.openPeer(peer.id)
}
)
controller.present(toastScreen, in: .window(.root))
view.avatarNode.isHidden = true
component.completion(peer.id)
}
self.animateOut {
if let controller = self.environment?.controller() {
controller.dismiss()
@ -158,6 +249,7 @@ private final class QuickShareScreenComponent: Component {
} else {
self.environment?.controller()?.dismiss()
}
component.ready.set(.single(true))
})
component.gesture.externalUpdated = { [weak self] view, point in
@ -216,7 +308,19 @@ private final class QuickShareScreenComponent: Component {
let padding: CGFloat = 5.0
let spacing: CGFloat = 7.0
let itemSize = CGSize(width: 38.0, height: 38.0)
let itemsCount = 5
let selectedItemSize = CGSize(width: 60.0, height: 60.0)
let itemsCount = self.peers?.count ?? 5
let widthExtension: CGFloat = self.selectedPeerId != nil ? selectedItemSize.width - itemSize.width : 0.0
let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0 + widthExtension, height: itemSize.height + padding * 2.0)
let contentRect = CGRect(
origin: CGPoint(
x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)),
y: sourceRect.minY - size.height - padding * 2.0
),
size: size
)
var itemFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: itemSize)
if let peers = self.peers {
@ -236,13 +340,18 @@ private final class QuickShareScreenComponent: Component {
isFocused = peer.id == selectedPeerId
}
let effectiveItemSize = isFocused == true ? selectedItemSize : itemSize
let effectiveItemFrame = CGRect(origin: itemFrame.origin.offsetBy(dx: 0.0, dy: itemSize.height - effectiveItemSize.height), size: effectiveItemSize)
let _ = componentView.update(
transition: componentTransition,
component: AnyComponent(
ItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: peer,
safeInsets: UIEdgeInsets(top: 0.0, left: contentRect.minX + effectiveItemFrame.minX, bottom: 0.0, right: availableSize.width - contentRect.maxX + contentRect.width - effectiveItemFrame.maxX),
isFocused: isFocused
)
),
@ -253,29 +362,29 @@ private final class QuickShareScreenComponent: Component {
if view.superview == nil {
self.containerView.addSubview(view)
}
componentTransition.setFrame(view: view, frame: itemFrame)
componentTransition.setScale(view: view, scale: effectiveItemSize.width / selectedItemSize.width)
componentTransition.setBounds(view: view, bounds: CGRect(origin: .zero, size: selectedItemSize))
componentTransition.setPosition(view: view, position: effectiveItemFrame.center)
}
itemFrame.origin.x += itemSize.width + spacing
itemFrame.origin.x += effectiveItemFrame.width + spacing
}
}
let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0, height: itemSize.height + padding * 2.0)
let contentRect = CGRect(
origin: CGPoint(
x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)),
y: sourceRect.minY - size.height - padding * 2.0
),
size: size
)
self.containerView.layer.cornerRadius = size.height / 2.0
self.backgroundView.layer.cornerRadius = size.height / 2.0
self.backgroundTintView.layer.cornerRadius = size.height / 2.0
transition.setFrame(view: self.backgroundView, frame: contentRect)
transition.setFrame(view: self.containerView, frame: contentRect)
self.backgroundView.update(size: contentRect.size, cornerRadius: size.height / 2.0, transition: transition.containedViewLayoutTransition)
self.backgroundView.update(size: contentRect.size, cornerRadius: 0.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundTintView, frame: CGRect(origin: .zero, size: contentRect.size))
let shadowInset: CGFloat = 15.0
let shadowColor = UIColor(white: 0.0, alpha: 0.4)
if self.backgroundShadowLayer.contents == nil, let image = generateBubbleShadowImage(shadow: shadowColor, diameter: 46.0, shadowBlur: shadowInset) {
ASDisplayNodeSetResizableContents(self.backgroundShadowLayer, image)
}
transition.setFrame(layer: self.backgroundShadowLayer, frame: contentRect.insetBy(dx: -shadowInset, dy: -shadowInset))
return availableSize
}
}
@ -293,17 +402,29 @@ public class QuickShareScreen: ViewControllerComponentContainer {
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
private let readyValue = Promise<Bool>()
override public var ready: Promise<Bool> {
return self.readyValue
}
public init(
context: AccountContext,
sourceNode: ASDisplayNode,
gesture: ContextGesture
gesture: ContextGesture,
openPeer: @escaping (EnginePeer.Id) -> Void,
completion: @escaping (EnginePeer.Id) -> Void
) {
let componentReady = Promise<Bool>()
super.init(
context: context,
component: QuickShareScreenComponent(
context: context,
sourceNode: sourceNode,
gesture: gesture
gesture: gesture,
openPeer: openPeer,
completion: completion,
ready: componentReady
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
@ -311,6 +432,8 @@ public class QuickShareScreen: ViewControllerComponentContainer {
updatedPresentationData: nil
)
self.navigationPresentation = .flatModal
self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
}
required public init(coder aDecoder: NSCoder) {
@ -360,18 +483,24 @@ public class QuickShareScreen: ViewControllerComponentContainer {
private final class ItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peer: EnginePeer
let safeInsets: UIEdgeInsets
let isFocused: Bool?
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: EnginePeer,
safeInsets: UIEdgeInsets,
isFocused: Bool?
) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.safeInsets = safeInsets
self.isFocused = isFocused
}
@ -379,6 +508,9 @@ private final class ItemComponent: Component {
if lhs.peer != rhs.peer {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.isFocused != rhs.isFocused {
return false
}
@ -386,7 +518,7 @@ private final class ItemComponent: Component {
}
final class View: UIView {
private let avatarNode: AvatarNode
fileprivate let avatarNode: AvatarNode
private let backgroundNode: NavigationBackgroundNode
private let text = ComponentView<Empty>()
@ -415,11 +547,13 @@ private final class ItemComponent: Component {
self.isUpdating = false
}
let size = CGSize(width: 60.0, height: 60.0)
var title = component.peer.compactDisplayTitle
var overrideImage: AvatarNodeImageOverride?
if component.peer.id == component.context.account.peerId {
overrideImage = .savedMessagesIcon
title = "Saved Messages"
title = component.strings.DialogList_SavedMessages
}
self.avatarNode.setPeer(
@ -430,22 +564,16 @@ private final class ItemComponent: Component {
synchronousLoad: true
)
self.avatarNode.view.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
self.avatarNode.view.bounds = CGRect(origin: .zero, size: availableSize)
self.avatarNode.updateSize(size: availableSize)
self.avatarNode.view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.avatarNode.view.bounds = CGRect(origin: .zero, size: size)
self.avatarNode.updateSize(size: size)
var scale: CGFloat = 1.0
var alpha: CGFloat = 1.0
var textAlpha: CGFloat = 0.0
var textOffset: CGFloat = 6.0
if let isFocused = component.isFocused {
scale = isFocused ? 1.1 : 1.0
alpha = isFocused ? 1.0 : 0.6
textAlpha = isFocused ? 1.0 : 0.0
textOffset = isFocused ? 0.0 : 6.0
}
transition.setScale(view: self.avatarNode.view, scale: scale)
transition.setAlpha(view: self.avatarNode.view, alpha: alpha)
let textSize = self.text.update(
transition: .immediate,
@ -459,10 +587,27 @@ private final class ItemComponent: Component {
if textView.superview == nil {
self.addSubview(textView)
}
let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: -16.0 - textSize.height + textOffset), size: textSize)
let initialX = floor((size.width - textSize.width) / 2.0)
var textFrame = CGRect(origin: CGPoint(x: initialX, y: -13.0 - textSize.height + textOffset), size: textSize)
let sideInset: CGFloat = 8.0
let textPadding: CGFloat = 8.0
let leftDistanceToEdge = 0.0 - textFrame.minX
let rightDistanceToEdge = textFrame.maxX - size.width
let leftSafeInset = component.safeInsets.left - textPadding - sideInset
let rightSafeInset = component.safeInsets.right - textPadding - sideInset
if leftSafeInset < leftDistanceToEdge {
textFrame.origin.x = -leftSafeInset
}
if rightSafeInset < rightDistanceToEdge {
textFrame.origin.x = size.width + rightSafeInset - textFrame.width
}
transition.setFrame(view: textView, frame: textFrame)
let backgroundFrame = textFrame.insetBy(dx: -7.0, dy: -3.0)
let backgroundFrame = textFrame.insetBy(dx: -textPadding, dy: -3.0 - UIScreenPixel)
transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame)
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: .immediate)
self.backgroundNode.updateColor(color: component.theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillStatic, enableBlur: true, transition: .immediate)
@ -471,7 +616,7 @@ private final class ItemComponent: Component {
transition.setAlpha(view: self.backgroundNode.view, alpha: textAlpha)
}
return availableSize
return size
}
}
@ -483,3 +628,17 @@ private final class ItemComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(shadow.cgColor)
context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
})?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0))
}

View File

@ -0,0 +1,372 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ViewControllerComponent
import AccountContext
import MultilineTextComponent
import AvatarNode
import Markdown
import LottieComponent
private final class QuickShareToastScreenComponent: Component {
let context: AccountContext
let peer: EnginePeer
let sourceFrame: CGRect
let action: () -> Void
init(
context: AccountContext,
peer: EnginePeer,
sourceFrame: CGRect,
action: @escaping () -> Void
) {
self.context = context
self.peer = peer
self.sourceFrame = sourceFrame
self.action = action
}
static func ==(lhs: QuickShareToastScreenComponent, rhs: QuickShareToastScreenComponent) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.sourceFrame != rhs.sourceFrame {
return false
}
return true
}
final class View: UIView {
private let contentView: UIView
private let backgroundView: BlurredBackgroundView
private let avatarNode: AvatarNode
private let animation = ComponentView<Empty>()
private let content = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: QuickShareToastScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
private var doneTimer: Foundation.Timer?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.contentView = UIView()
self.contentView.isUserInteractionEnabled = false
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.contentView)
self.contentView.addSubview(self.avatarNode.view)
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture)))
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.doneTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.backgroundView.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
@objc private func tapGesture() {
guard let component = self.component else {
return
}
component.action()
self.environment?.controller()?.dismiss()
}
func animateIn() {
guard let component = self.component else {
return
}
func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(CGPoint(x: x, y: y))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(CGPoint(x: x, y: y))
}
}
return keyframes
}
let playIconAnimation: (Double) -> Void = { duration in
self.avatarNode.contentNode.alpha = 0.0
self.avatarNode.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
self.avatarNode.contentNode.layer.animateScale(from: 1.0, to: 0.01, duration: duration, removeOnCompletion: false)
if let view = self.animation.view as? LottieComponent.View {
view.alpha = 1.0
view.playOnce()
}
}
if component.peer.id == component.context.account.peerId {
playIconAnimation(0.2)
}
let offset = self.bounds.height - self.backgroundView.frame.minY
self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
if component.peer.id != component.context.account.peerId {
playIconAnimation(0.1)
}
HapticFeedback().success()
})
if let component = self.component {
let fromPoint = self.avatarNode.view.convert(component.sourceFrame.center, from: nil).offsetBy(dx: 0.0, dy: -offset)
let positionValues = generateAvatarParabollicMotionKeyframes(from: fromPoint, to: .zero, elevation: 20.0)
self.avatarNode.layer.animateKeyframes(values: positionValues.map { NSValue(cgPoint: $0) }, duration: 0.35, keyPath: "position", additive: true)
self.avatarNode.layer.animateScale(from: component.sourceFrame.width / self.avatarNode.bounds.width, to: 1.0, duration: 0.35)
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.5))
}
}
func animateOut(completion: @escaping () -> Void) {
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in
completion()
})
self.backgroundView.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
func update(component: QuickShareToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
if self.component == nil {
self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
})
}
self.component = component
self.environment = environment
self.state = state
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
let tabBarHeight: CGFloat
if !environment.safeInsets.left.isZero {
tabBarHeight = 34.0 + environment.safeInsets.bottom
} else {
tabBarHeight = 49.0 + environment.safeInsets.bottom
}
let containerInsets = UIEdgeInsets(
top: environment.safeInsets.top,
left: environment.safeInsets.left + 12.0,
bottom: tabBarHeight + 3.0,
right: environment.safeInsets.right + 12.0
)
let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom)
let spacing: CGFloat = 8.0
let iconSize = CGSize(width: 30.0, height: 30.0)
let tooltipText: String
var overrideImage: AvatarNodeImageOverride?
var animationName: String = "anim_forward"
if component.peer.id == component.context.account.peerId {
tooltipText = environment.strings.Conversation_ForwardTooltip_SavedMessages_One
overrideImage = .savedMessagesIcon
animationName = "anim_savedmessages"
} else {
tooltipText = environment.strings.Conversation_ForwardTooltip_Chat_One(component.peer.compactDisplayTitle).string
}
let contentSize = self.content.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .markdown(
text: tooltipText,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)),
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
linkAttribute: { _ in return nil })
))),
environment: {},
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - 16.0, height: availableContentSize.height)
)
var contentHeight: CGFloat = 0.0
contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
self.avatarNode.setPeer(context: component.context, theme: environment.theme, peer: component.peer, overrideImage: overrideImage, synchronousLoad: true)
self.avatarNode.updateSize(size: avatarFrame.size)
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
let _ = self.animation.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: animationName
),
size: CGSize(width: 38.0, height: 38.0),
loop: false
)),
environment: {},
containerSize: iconSize
)
if let animationView = self.animation.view {
if animationView.superview == nil {
animationView.alpha = 0.0
self.avatarNode.view.addSubview(animationView)
}
animationView.frame = CGRect(origin: .zero, size: iconSize).insetBy(dx: -2.0, dy: -2.0)
}
if let contentView = self.content.view {
if contentView.superview == nil {
self.contentView.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize))
}
let size = CGSize(width: availableContentSize.width, height: contentHeight)
let backgroundFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size)
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: backgroundFrame.size))
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class QuickShareToastScreen: ViewControllerComponentContainer {
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
init(
context: AccountContext,
peer: EnginePeer,
sourceFrame: CGRect,
action: @escaping () -> Void
) {
super.init(
context: context,
component: QuickShareToastScreenComponent(
context: context,
peer: peer,
sourceFrame: sourceFrame,
action: action
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
presentationMode: .default,
updatedPresentationData: nil
)
self.navigationPresentation = .flatModal
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.processedDidAppear {
self.processedDidAppear = true
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
componentView.animateIn()
}
}
}
private func superDismiss() {
super.dismiss()
}
override func dismiss(completion: (() -> Void)? = nil) {
if !self.processedDidDisappear {
self.processedDidDisappear = true
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
if let self {
self.superDismiss()
}
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "StoryPeerListMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "StoryPeerListBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.StoryPeerList</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>StoryPeerList</string>
"""
)
apple_resource_bundle(
name = "StoryPeerListBundle",
infoplists = [
":StoryPeerListBundleInfoPlist",
],
resources = [
":StoryPeerListMetalResources",
],
)
swift_library(
name = "StoryPeerListComponent",
@ -8,9 +48,13 @@ swift_library(
]),
copts = [
"-warnings-as-errors",
],
data = [
":StoryPeerListBundle",
],
deps = [
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/Components/BundleIconComponent",

View File

@ -0,0 +1,109 @@
#include <metal_stdlib>
using namespace metal;
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
vertex QuadVertexOut cameraBlobVertex(
constant float4 &rect [[ buffer(0) ]],
uint vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
#define BindingDistance 0.45
#define AARadius 2.0
#define PersistenceFactor 1.1
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
h = pow(h, 0.6);
return mix(a, b, h) - k * h * (1.0 - h) * 1.0;
}
float sdfCircle(float2 uv, float2 position, float radius) {
return length(uv - position) - radius;
}
float dynamicBinding(float2 primaryPos, float2 secondaryPos) {
float distance = length(primaryPos - secondaryPos);
return BindingDistance * (1.0 + PersistenceFactor * smoothstep(0.0, 2.0, distance));
}
float map(float2 uv, float2 primaryParameters, float2 primaryOffset, float2 secondaryParameters, float2 secondaryOffset) {
float primary = sdfCircle(uv, primaryOffset, primaryParameters.x);
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
float bindDist = dynamicBinding(primaryOffset, secondaryOffset);
float metaballs = 1.0;
metaballs = smin(metaballs, primary, bindDist);
metaballs = smin(metaballs, secondary, bindDist);
return metaballs;
}
fragment half4 cameraBlobFragment(
QuadVertexOut in [[stage_in]],
constant float2 &primaryParameters [[buffer(0)]],
constant float2 &primaryOffset [[buffer(1)]],
constant float2 &secondaryParameters [[buffer(2)]],
constant float2 &secondaryOffset [[buffer(3)]],
constant float2 &resolution [[buffer(4)]]
) {
float aspectRatio = resolution.x / resolution.y;
float2 uv = in.uv * 2.0 - 1.0;
uv.x *= aspectRatio;
float t = AARadius / resolution.y;
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
if (primaryParameters.y > 0) {
float innerHoleRadius = primaryParameters.y;
float hole = smoothstep(-t, t, length(uv - primaryOffset) - innerHoleRadius);
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
c *= mix(1.0, hole, primaryInfluence);
} else if (primaryParameters.y < 0) {
float cutRadius = abs(primaryParameters.y);
float2 primaryFeatheredOffset = primaryOffset;
primaryFeatheredOffset.x *= 1.129;
float distFromCenter = length(uv - primaryFeatheredOffset);
float gradientWidth = 0.21;
float featheredEdge = smoothstep(cutRadius - gradientWidth, cutRadius + gradientWidth, distFromCenter);
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
float horizontalFade = 1.0;
float rightEdgePosition = 0.94 * aspectRatio;
if (uv.x > rightEdgePosition) {
horizontalFade = 1.0 - smoothstep(0.0, 0.22, uv.x - rightEdgePosition);
}
c *= mix(1.0, featheredEdge, primaryInfluence) * horizontalFade;
}
return half4(0.0, 0.0, 0.0, c);
}

View File

@ -0,0 +1,545 @@
import Foundation
import Display
import Metal
import MetalKit
import MetalEngine
import ComponentFlow
import TelegramPresentationData
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: StoryBlobLayer.self)
guard let path = mainBundle.path(forResource: "StoryPeerListBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}
private final class PropertyAnimation<T: Interpolatable> {
let from: T
let to: T
let animation: ComponentTransition.Animation
let startTimestamp: Double
private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable
init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) {
self.from = fromValue
self.to = toValue
self.animation = animation
self.startTimestamp = startTimestamp
self.interpolator = T.interpolator()
}
func valueAt(_ t: CGFloat) -> Interpolatable {
if t <= 0.0 {
return self.from
} else if t >= 1.0 {
return self.to
} else {
return self.interpolator(self.from, self.to, t)
}
}
}
private final class AnimatableProperty<T: Interpolatable> {
var presentationValue: T
var value: T
private var animation: PropertyAnimation<T>?
init(value: T) {
self.value = value
self.presentationValue = value
}
func update(value: T, transition: ComponentTransition = .immediate) {
let currentTimestamp = CACurrentMediaTime()
if case .none = transition.animation {
if let animation = self.animation, case let .curve(duration, curve) = animation.animation {
self.value = value
let elapsed = duration - (currentTimestamp - animation.startTimestamp)
if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 {
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp)
} else {
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp)
}
} else {
self.value = value
self.presentationValue = value
self.animation = nil
}
} else {
self.value = value
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp)
}
}
func tick(timestamp: Double) -> Bool {
guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else {
return false
}
let timeFromStart = timestamp - animation.startTimestamp
var t = max(0.0, timeFromStart / duration)
switch curve {
case .linear:
break
case .easeInOut:
t = listViewAnimationCurveEaseInOut(t)
case .spring:
t = lookupSpringValue(t)
case let .custom(x1, y1, x2, y2):
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
}
self.presentationValue = animation.valueAt(t) as! T
if timeFromStart <= duration {
return true
}
self.animation = nil
return false
}
}
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
let table: [(CGFloat, CGFloat)] = [
(0.0, 0.0),
(0.0625, 0.1123005598783493),
(0.125, 0.31598418951034546),
(0.1875, 0.5103585720062256),
(0.25, 0.6650152802467346),
(0.3125, 0.777747631072998),
(0.375, 0.8557760119438171),
(0.4375, 0.9079672694206238),
(0.5, 0.942038357257843),
(0.5625, 0.9638798832893372),
(0.625, 0.9776856303215027),
(0.6875, 0.9863143563270569),
(0.75, 0.991658091545105),
(0.8125, 0.9949421286582947),
(0.875, 0.9969474077224731),
(0.9375, 0.9981651306152344),
(1.0, 1.0)
]
for i in 0 ..< table.count - 2 {
let lhs = table[i]
let rhs = table[i + 1]
if t >= lhs.0 && t <= rhs.0 {
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
return value
}
}
return 1.0
}
final class StoryBlobLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
private final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "cameraBlobVertex"),
let fragmentFunction = library.makeFunction(name: "cameraBlobFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
private var primarySize = AnimatableProperty<CGFloat>(value: 1.0)
private var primaryHoleSize = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.85)
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
private var hasActiveAnimations: Bool = false
override init() {
super.init()
self.isOpaque = false
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.updateAnimations()
if self.hasActiveAnimations {
self.setNeedsUpdate()
}
}
}
self.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = nil
}
}
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? StoryBlobLayer {
self.primarySize = layer.primarySize
self.primaryHoleSize = layer.primaryHoleSize
self.primaryOffsetX = layer.primaryOffsetX
self.primaryOffsetY = layer.primaryOffsetY
self.secondarySize = layer.secondarySize
self.secondaryOffsetX = layer.secondaryOffsetX
self.secondaryOffsetY = layer.secondaryOffsetY
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePrimarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedSize = size / self.bounds.height
self.primarySize.update(value: mappedSize, transition: transition)
self.setNeedsUpdate()
}
func updatePrimaryHoleSize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedSize = size / self.bounds.height
self.primaryHoleSize.update(value: mappedSize, transition: transition)
self.setNeedsUpdate()
}
func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.height * 2.0
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.width > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.width * 2.0
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
func updateSecondarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedSize = size / self.bounds.height
self.secondarySize.update(value: mappedSize, transition: transition)
self.setNeedsUpdate()
}
func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.height * 2.0
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.width > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.width * 2.0
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
private func updateAnimations() {
let properties = [
self.primarySize,
self.primaryHoleSize,
self.primaryOffsetX,
self.primaryOffsetY,
self.secondarySize,
self.secondaryOffsetX,
self.secondaryOffsetY,
]
let timestamp = CACurrentMediaTime()
var hasAnimations = false
for property in properties {
if property.tick(timestamp: timestamp) {
hasAnimations = true
}
}
self.hasActiveAnimations = hasAnimations
}
func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
return
}
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, commands: { encoder, placement in
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
var primaryParameters = simd_float2(
Float(self.primarySize.presentationValue),
Float(self.primaryHoleSize.presentationValue)
)
encoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float2>.size, index: 0)
var primaryOffset = simd_float2(
Float(self.primaryOffsetX.presentationValue),
Float(self.primaryOffsetY.presentationValue)
)
encoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 1)
var secondaryParameters = simd_float2(
Float(self.secondarySize.presentationValue),
Float(0)
)
encoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float2>.size, index: 2)
var secondaryOffset = simd_float2(
Float(self.secondaryOffsetX.presentationValue),
Float(self.secondaryOffsetY.presentationValue)
)
encoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 3)
var resolution = simd_float2(
Float(drawableSize.width),
Float(drawableSize.height)
)
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_float2>.size, index: 4)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}
final class StoryComposeLayer: CALayer {
private let theme: PresentationTheme?
private let strings: PresentationStrings?
private let blobLayer = SimpleLayer()
private let maskLayer = StoryBlobLayer()
private let backgroundLayer = SimpleGradientLayer()
private let foregroundLayer = SimpleGradientLayer()
private let iconLayer = SimpleLayer()
private let labelLayer = SimpleLayer()
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
super.init()
self.addSublayer(self.blobLayer)
self.blobLayer.mask = self.maskLayer
self.blobLayer.masksToBounds = true
self.backgroundLayer.type = .axial
self.backgroundLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.backgroundLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.blobLayer.addSublayer(self.backgroundLayer)
self.foregroundLayer.type = .axial
self.foregroundLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.foregroundLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
self.blobLayer.addSublayer(self.foregroundLayer)
self.iconLayer.contents = generateAddIcon(color: theme.list.itemCheckColors.foregroundColor)?.cgImage
self.iconLayer.opacity = 0.0
self.blobLayer.addSublayer(self.iconLayer)
if let image = generateAddLabel(strings: strings, color: theme.list.itemPrimaryTextColor) {
self.labelLayer.contents = image.cgImage
self.labelLayer.bounds = CGRect(origin: .zero, size: image.size)
self.addSublayer(self.labelLayer)
self.labelLayer.opacity = 0.0
}
if let blurEffect = CALayer.blur() {
self.iconLayer.filters = [blurEffect]
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
self.labelLayer.filters = [blurEffect]
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
}
}
override init(layer: Any) {
if let layer = layer as? StoryComposeLayer {
self.theme = layer.theme
self.strings = layer.strings
} else {
self.theme = nil
self.strings = nil
}
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateOffset(_ offset: CGFloat, baseSize: CGFloat, colors: [CGColor], transition: ComponentTransition = .immediate) {
self.backgroundLayer.colors = colors
self.foregroundLayer.locations = [0.0, 0.68, 1.0]
if let theme = self.theme {
let fillColor = theme.list.itemCheckColors.fillColor
self.foregroundLayer.colors = [
fillColor.cgColor,
fillColor.withAlphaComponent(0.0).cgColor,
fillColor.withAlphaComponent(0.0).cgColor
]
}
var holeSize = baseSize > 52.0 ? 58.0 : -60.0
var primaryBaseScale = 1.0
var secondaryBaseScale = 0.68
if holeSize < 0.0 {
if offset > 48.0 {
holeSize -= min(14.0, (max(0.0, (offset - 48.0)) / 20.0) * 14.0)
}
primaryBaseScale = 0.96 + min(1.0, max(0.0, (offset - 20.0)) / 10.0) * 0.04
secondaryBaseScale = 0.62
}
var secondaryScale = secondaryBaseScale
if offset < 20.0 {
secondaryScale += (offset / 20.0) * 0.2
} else {
secondaryScale = min(1.0, max(secondaryBaseScale + 0.2, offset / 75.0))
}
self.maskLayer.updatePrimaryOffsetX(85.0, transition: transition)
self.maskLayer.updatePrimarySize(baseSize * primaryBaseScale, transition: transition)
self.maskLayer.updateSecondaryOffsetX(85.0 - offset, transition: transition)
self.maskLayer.updatePrimaryHoleSize(holeSize, transition: transition)
self.maskLayer.updateSecondarySize(55.0 * secondaryScale, transition: transition)
if holeSize < 0.0 {
transition.setAlpha(layer: self.blobLayer, alpha: min(1.0, max(0.0, offset / 10.0)))
}
let layerX = self.bounds.width - offset + 5.0
transition.setPosition(layer: self.iconLayer, position: CGPoint(x: layerX, y: self.bounds.height / 2.0))
transition.setPosition(layer: self.labelLayer, position: CGPoint(x: layerX, y: self.bounds.height + 13.0 - UIScreenPixel))
let iconOffset = max(0.0, offset - 35.0)
let alpha = max(0.0, min(1.0, iconOffset / 15.0))
let blurRadius = 8.0 - min(8.0, (iconOffset / 20.0) * 8.0)
for layer in [self.iconLayer, self.labelLayer] {
layer.setValue(blurRadius as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius")
transition.setAlpha(layer: layer, alpha: alpha)
}
}
override func layoutSublayers() {
super.layoutSublayers()
self.blobLayer.frame = self.bounds
self.maskLayer.frame = self.bounds
self.backgroundLayer.frame = self.bounds
self.foregroundLayer.frame = self.bounds
self.iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
}
}
private func generateAddIcon(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(3.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 15.0, y: 5.5))
context.addLine(to: CGPoint(x: 15.0, y: 24.5))
context.strokePath()
context.move(to: CGPoint(x: 5.5, y: 15.0))
context.addLine(to: CGPoint(x: 24.5, y: 15.0))
context.strokePath()
})
}
private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? {
let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
textRect.size.width = ceil(textRect.size.width)
textRect.size.height = ceil(textRect.size.height)
return generateImage(textRect.size, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
UIGraphicsPushContext(context)
titleString.draw(in: textRect)
UIGraphicsPopContext()
})
}

View File

@ -60,6 +60,7 @@ public final class StoryPeerListComponent: Component {
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public let openStatusSetup: (UIView) -> Void
public let lockAction: () -> Void
public let composeAction: (CGFloat) -> Void
public init(
externalState: ExternalState,
@ -81,7 +82,8 @@ public final class StoryPeerListComponent: Component {
peerAction: @escaping (EnginePeer?) -> Void,
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void,
openStatusSetup: @escaping (UIView) -> Void,
lockAction: @escaping () -> Void
lockAction: @escaping () -> Void,
composeAction: @escaping (CGFloat) -> Void
) {
self.externalState = externalState
self.context = context
@ -103,6 +105,7 @@ public final class StoryPeerListComponent: Component {
self.contextPeerAction = contextPeerAction
self.openStatusSetup = openStatusSetup
self.lockAction = lockAction
self.composeAction = composeAction
}
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
@ -162,7 +165,6 @@ public final class StoryPeerListComponent: Component {
private final class VisibleItem {
let view = ComponentView<Empty>()
var hasBlur: Bool = false
init() {
}
@ -360,6 +362,8 @@ public final class StoryPeerListComponent: Component {
private var sharedBlurEffect: NSObject?
private var willComposeOnRelease = false
public override init(frame: CGRect) {
self.collapsedButton = HighlightableButton()
@ -561,14 +565,62 @@ public final class StoryPeerListComponent: Component {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
let willComposeOnRelease = scrollView.contentOffset.x <= -70.0
if self.willComposeOnRelease != willComposeOnRelease {
self.willComposeOnRelease = willComposeOnRelease
if willComposeOnRelease {
HapticFeedback().tap()
} else {
HapticFeedback().impact(.veryLight)
}
}
if scrollView.isScrollEnabled && scrollView.isTracking, scrollView.contentOffset.x <= -85.0 {
scrollView.isScrollEnabled = false
scrollView.panGestureRecognizer.isEnabled = false
scrollView.panGestureRecognizer.isEnabled = true
scrollView.contentOffset = CGPoint(x: -85.0, y: 0.0)
self.willComposeOnRelease = false
Queue.mainQueue().after(0.5) {
scrollView.isScrollEnabled = true
scrollView.contentOffset = .zero
}
if let component = self.component {
HapticFeedback().tap()
component.composeAction(abs(scrollView.contentOffset.x))
}
}
}
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !self.ignoreScrolling {
if scrollView.isScrollEnabled && scrollView.contentOffset.x <= -70.0 {
scrollView.isScrollEnabled = false
scrollView.panGestureRecognizer.isEnabled = false
scrollView.panGestureRecognizer.isEnabled = true
scrollView.contentOffset = CGPoint(x: max(-85.0, scrollView.contentOffset.x), y: 0.0)
self.willComposeOnRelease = false
Queue.mainQueue().after(0.5) {
scrollView.isScrollEnabled = true
scrollView.contentOffset = .zero
}
if let component = self.component {
HapticFeedback().tap()
component.composeAction(abs(scrollView.contentOffset.x))
}
}
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
let titleIconSpacing: CGFloat = 4.0
let titleIndicatorSpacing: CGFloat = 8.0
@ -1072,6 +1124,11 @@ public final class StoryPeerListComponent: Component {
totalCount = itemSet.storyCount
unseenCount = itemSet.unseenCount
var composeContentOffset: CGFloat?
if peer.id == component.context.account.peerId && collapsedState.sideAlphaFraction == 1.0 && self.scrollView.contentOffset.x < 0.0 {
composeContentOffset = self.scrollView.contentOffset.x * -1.0
}
let _ = visibleItem.view.update(
transition: itemTransition,
component: AnyComponent(StoryPeerListItemComponent(
@ -1090,6 +1147,7 @@ public final class StoryPeerListComponent: Component {
expandEffectFraction: collapsedState.expandEffectFraction,
leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance,
composeContentOffset: composeContentOffset,
action: component.peerAction,
contextGesture: component.contextPeerAction
)),
@ -1228,6 +1286,7 @@ public final class StoryPeerListComponent: Component {
expandEffectFraction: collapsedState.expandEffectFraction,
leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance,
composeContentOffset: nil,
action: component.peerAction,
contextGesture: component.contextPeerAction
)),

View File

@ -380,6 +380,7 @@ public final class StoryPeerListItemComponent: Component {
public let expandEffectFraction: CGFloat
public let leftNeighborDistance: CGPoint?
public let rightNeighborDistance: CGPoint?
public let composeContentOffset: CGFloat?
public let action: (EnginePeer) -> Void
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
@ -399,6 +400,7 @@ public final class StoryPeerListItemComponent: Component {
expandEffectFraction: CGFloat,
leftNeighborDistance: CGPoint?,
rightNeighborDistance: CGPoint?,
composeContentOffset: CGFloat?,
action: @escaping (EnginePeer) -> Void,
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
) {
@ -417,6 +419,7 @@ public final class StoryPeerListItemComponent: Component {
self.expandEffectFraction = expandEffectFraction
self.leftNeighborDistance = leftNeighborDistance
self.rightNeighborDistance = rightNeighborDistance
self.composeContentOffset = composeContentOffset
self.action = action
self.contextGesture = contextGesture
}
@ -467,6 +470,9 @@ public final class StoryPeerListItemComponent: Component {
if lhs.rightNeighborDistance != rhs.rightNeighborDistance {
return false
}
if lhs.composeContentOffset != rhs.composeContentOffset {
return false
}
return true
}
@ -479,6 +485,7 @@ public final class StoryPeerListItemComponent: Component {
private let button: HighlightTrackingButton
fileprivate var composeLayer: StoryComposeLayer?
fileprivate let avatarContent: PortalSourceView
private let avatarContainer: UIView
private let avatarBackgroundContainer: UIView
@ -494,12 +501,11 @@ public final class StoryPeerListItemComponent: Component {
private let indicatorShapeSeenLayer: SimpleShapeLayer
private let indicatorShapeUnseenLayer: SimpleShapeLayer
private let title = ComponentView<Empty>()
private let composeTitle = ComponentView<Empty>()
private var component: StoryPeerListItemComponent?
private weak var componentState: EmptyComponentState?
private var demoLoading = false
public override init(frame: CGRect) {
self.backgroundContainer = UIView()
self.backgroundContainer.isUserInteractionEnabled = false
@ -960,6 +966,32 @@ public final class StoryPeerListItemComponent: Component {
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height))
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
var baseSize: CGFloat = 60.0
var effectiveColors = component.unseenCount > 0 ? unseenColors : seenColors
if self.avatarAddBadgeView != nil {
baseSize = 52.0
effectiveColors = [component.theme.list.itemCheckColors.fillColor.cgColor, component.theme.list.itemCheckColors.fillColor.cgColor]
}
if let composeContentOffset = component.composeContentOffset {
let composeLayer: StoryComposeLayer
if let current = self.composeLayer {
composeLayer = current
} else {
composeLayer = StoryComposeLayer(theme: component.theme, strings: component.strings)
self.composeLayer = composeLayer
self.layer.addSublayer(composeLayer)
}
composeLayer.frame = CGRect(origin: CGPoint(x: size.width - 195.0, y: 0.0), size: CGSize(width: 160.0, height: 60.0))
composeLayer.updateOffset(composeContentOffset, baseSize: baseSize, colors: effectiveColors, transition: transition)
} else if let composeLayer = self.composeLayer {
self.composeLayer = nil
composeLayer.updateOffset(0.0, baseSize: baseSize, colors: effectiveColors, transition: .easeInOut(duration: 0.2))
Queue.mainQueue().after(0.21, {
composeLayer.removeFromSuperlayer()
})
}
return availableSize
}
}

View File

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

View File

@ -292,11 +292,20 @@ extension ChatControllerImpl {
}
}
var keepDefaultContentTouches = false
for media in message.media {
if media is TelegramMediaImage {
keepDefaultContentTouches = true
} else if let file = media as? TelegramMediaFile, file.isVideo {
keepDefaultContentTouches = true
}
}
let source: ContextContentSource
if let location = location {
source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
} else {
source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll))
source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll, keepDefaultContentTouches: keepDefaultContentTouches))
}
self.canReadHistory.set(false)

View File

@ -1,15 +1,52 @@
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import ContextUI
import QuickShareScreen
extension ChatControllerImpl {
func displayQuickShare(node: ASDisplayNode, gesture: ContextGesture) {
guard !"".isEmpty else {
return
}
let controller = QuickShareScreen(context: self.context, sourceNode: node, gesture: gesture)
func displayQuickShare(id: EngineMessage.Id, node: ASDisplayNode, gesture: ContextGesture) {
let controller = QuickShareScreen(
context: self.context,
sourceNode: node,
gesture: gesture,
openPeer: { [weak self] peerId in
guard let self else {
return
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self else {
return
}
self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
})
},
completion: { [weak self] peerId in
guard let self else {
return
}
let enqueueMessage = StandaloneSendEnqueueMessage(
content: .forward(forward: StandaloneSendEnqueueMessage.Forward(
sourceId: id,
threadId: nil
)),
replyToMessageId: nil
)
let _ = (standaloneSendEnqueueMessages(
accountPeerId: self.context.account.peerId,
postbox: self.context.account.postbox,
network: self.context.account.network,
stateManager: self.context.account.stateManager,
auxiliaryMethods: self.context.account.auxiliaryMethods,
peerId: peerId,
threadId: nil,
messages: [enqueueMessage]
)).startStandalone()
}
)
self.presentInGlobalOverlay(controller)
}
}

View File

@ -250,7 +250,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var botStart: ChatControllerInitialBotStart?
var attachBotStart: ChatControllerInitialAttachBotStart?
var botAppStart: ChatControllerInitialBotAppStart?
let mode: ChatControllerPresentationMode
var mode: ChatControllerPresentationMode
let peerDisposable = MetaDisposable()
let titleDisposable = MetaDisposable()
@ -575,7 +575,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)?
public var purposefulAction: (() -> Void)?
public var dismissPreviewing: (() -> Void)?
public var dismissPreviewing: ((Bool) -> (() -> Void))?
var updatedClosedPinnedMessageId: ((MessageId) -> Void)?
var requestedUnpinAllMessages: ((Int, MessageId) -> Void)?
@ -893,6 +893,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false
}
if let contextController = self.currentContextController {
self.present(contextController, in: .window(.root))
Queue.mainQueue().after(0.15) {
contextController.dismiss(result: .dismissWithoutContent, completion: nil)
}
}
let mode = params.mode
let displayVoiceMessageDiscardAlert: () -> Bool = { [weak self] in
@ -1360,7 +1367,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let self else {
return
}
self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view)
if let contextController = self.currentContextController {
contextController.view.addSubview(view)
} else {
self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view)
}
}, openUrl: { [weak self] url in
self?.openUrl(url, concealed: false, skipConcealedAlert: isLocation, message: nil)
}, openPeer: { [weak self] peer, navigation in
@ -4850,11 +4861,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.playShakeAnimation()
}, displayQuickShare: { [weak self] node, gesture in
}, displayQuickShare: { [weak self] messageId, node, gesture in
guard let self else {
return
}
self.displayQuickShare(node: node, gesture: gesture)
self.displayQuickShare(id: messageId, node: node, gesture: gesture)
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
@ -7458,11 +7469,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
self.mode = mode
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
return $0.updatedMode(mode)
})
}
func animateFromPreviewing(transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)) {
guard let navigationController = self.effectiveNavigationController else {
return
}
self.mode = .standard(.default)
let completion = self.dismissPreviewing?(true)
let initialLayout = self.validLayout
let initialFrame = self.view.convert(self.view.bounds, to: navigationController.view)
navigationController.pushViewController(self, animated: false)
let updatedLayout = self.validLayout
let updatedFrame = self.view.frame
if let initialLayout, let updatedLayout, transition.isAnimated {
let initialView = self.view.superview
navigationController.view.addSubview(self.view)
self.view.clipsToBounds = true
self.view.frame = initialFrame
self.containerLayoutUpdated(initialLayout, transition: .immediate)
self.containerLayoutUpdated(updatedLayout, transition: transition)
self.view.layer.animate(from: 14.0, to: updatedLayout.deviceMetrics.screenCornerRadius, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
transition.updateFrame(view: self.view, frame: updatedFrame, completion: { _ in
initialView?.addSubview(self.view)
self.view.clipsToBounds = false
completion?()
})
transition.updateCornerRadius(layer: self.view.layer, cornerRadius: 0.0)
}
if let navigationBar = self.navigationBar {
let nodes = [
navigationBar.backButtonNode,
navigationBar.backButtonArrow,
navigationBar.badgeNode
]
for node in nodes {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
self.canReadHistory.set(true)
self.updateChatPresentationInterfaceState(transition: transition, interactive: false) { state in
return state.updatedMode(self.mode)
}
}
var chatDisplayNode: ChatControllerNode {
get {
return super.displayNode as! ChatControllerNode

View File

@ -3453,7 +3453,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == .ended {
self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view))
if case .standard(.previewing) = self.chatPresentationInterfaceState.mode {
self.controller?.animateFromPreviewing()
} else {
self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view))
}
}
}

View File

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

View File

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

View File

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

View File

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