diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 93aca41543..e6eeb1b791 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -36,6 +36,8 @@ 0958FBBB218AD6BC00E0CBD8 /* InstantPageFeedbackNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0958FBBA218AD6BC00E0CBD8 /* InstantPageFeedbackNode.swift */; }; 0958FBBD218B03CA00E0CBD8 /* InstantPageDetailsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0958FBBC218B03CA00E0CBD8 /* InstantPageDetailsNode.swift */; }; 09619B8E21A34C0100493558 /* InstantPageScrollableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09619B8D21A34C0100493558 /* InstantPageScrollableNode.swift */; }; + 09619B9521A4ABF600493558 /* InstantPageReferenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09619B9321A4ABF500493558 /* InstantPageReferenceController.swift */; }; + 09619B9621A4ABF600493558 /* InstantPageReferenceControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09619B9421A4ABF600493558 /* InstantPageReferenceControllerNode.swift */; }; 096C98BA21787A5C00C211FF /* LegacyBridgeAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096C98B921787A5C00C211FF /* LegacyBridgeAudio.swift */; }; 096C98BF21787C6700C211FF /* TGBridgeAudioEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 096C98BB21787C6600C211FF /* TGBridgeAudioEncoder.m */; }; 096C98C021787C6700C211FF /* TGBridgeAudioEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */; }; @@ -1096,6 +1098,8 @@ 0958FBBA218AD6BC00E0CBD8 /* InstantPageFeedbackNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageFeedbackNode.swift; sourceTree = ""; }; 0958FBBC218B03CA00E0CBD8 /* InstantPageDetailsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageDetailsNode.swift; sourceTree = ""; }; 09619B8D21A34C0100493558 /* InstantPageScrollableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageScrollableNode.swift; sourceTree = ""; }; + 09619B9321A4ABF500493558 /* InstantPageReferenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageReferenceController.swift; sourceTree = ""; }; + 09619B9421A4ABF600493558 /* InstantPageReferenceControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageReferenceControllerNode.swift; sourceTree = ""; }; 096C98B921787A5C00C211FF /* LegacyBridgeAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyBridgeAudio.swift; sourceTree = ""; }; 096C98BB21787C6600C211FF /* TGBridgeAudioEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBridgeAudioEncoder.m; sourceTree = ""; }; 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioEncoder.h; sourceTree = ""; }; @@ -3075,6 +3079,8 @@ D07827CC1E03F32C00071108 /* Instant Page */ = { isa = PBXGroup; children = ( + 09619B9321A4ABF500493558 /* InstantPageReferenceController.swift */, + 09619B9421A4ABF600493558 /* InstantPageReferenceControllerNode.swift */, D0215D371E040F53001A0B1E /* InstantPageNode.swift */, D0215D391E041003001A0B1E /* InstantPageLayout.swift */, D0215D3B1E041014001A0B1E /* InstantPageItem.swift */, @@ -5012,6 +5018,7 @@ D0EC6CF01EB9F58800EBF1C3 /* AutomaticMediaDownloadSettings.swift in Sources */, D0EC6CF11EB9F58800EBF1C3 /* GeneratedMediaStoreSettings.swift in Sources */, D0EC6CF21EB9F58800EBF1C3 /* VoiceCallSettings.swift in Sources */, + 09619B9621A4ABF600493558 /* InstantPageReferenceControllerNode.swift in Sources */, D0F8C397201774A200236FC5 /* FeedGroupingController.swift in Sources */, D0EC6CF31EB9F58800EBF1C3 /* PresentationThemeSettings.swift in Sources */, D067B4AD211C916300796039 /* TGChannelIntroController.m in Sources */, @@ -5527,6 +5534,7 @@ D0EC6DFE1EB9F58900EBF1C3 /* GalleryControllerPresentationState.swift in Sources */, D0E8B8BB2044780600605593 /* ItemListSecretChatKeyItem.swift in Sources */, D0EC6DFF1EB9F58900EBF1C3 /* GalleryItem.swift in Sources */, + 09619B9521A4ABF600493558 /* InstantPageReferenceController.swift in Sources */, D0EC6E001EB9F58900EBF1C3 /* GalleryItemNode.swift in Sources */, D048EA8B1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift in Sources */, D0EC6E011EB9F58900EBF1C3 /* GalleryPagerNode.swift in Sources */, diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 160c8c8306..e481a3e4f5 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1971,7 +1971,7 @@ func handlePeerInfoAboutTextAction(account: Account, peerId: PeerId, navigateDis ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) - ])]) + ])]) controller.present(actionSheet, in: .window(.root)) case let .mention(mention): let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) diff --git a/TelegramUI/ImageTransparency.swift b/TelegramUI/ImageTransparency.swift index 216f920630..22aede1b85 100644 --- a/TelegramUI/ImageTransparency.swift +++ b/TelegramUI/ImageTransparency.swift @@ -60,7 +60,7 @@ func imageHasTransparency(_ cgImage: CGImage) -> Bool { return false } -private func scaledContext(_ cgImage: CGImage, maxSize: CGSize) -> DrawingContext { +private func scaledDrawingContext(_ cgImage: CGImage, maxSize: CGSize) -> DrawingContext { var size = CGSize(width: cgImage.width, height: cgImage.height) if (size.width > maxSize.width && size.height > maxSize.height) { size = size.aspectFilled(maxSize) @@ -80,37 +80,40 @@ func imageRequiresInversion(_ cgImage: CGImage) -> Bool { return false } - let context = scaledContext(cgImage, maxSize: CGSize(width: 128.0, height: 128.0)) + let context = scaledDrawingContext(cgImage, maxSize: CGSize(width: 128.0, height: 128.0)) if let cgImage = context.generateImage()?.cgImage, let (histogramBins, alphaBinIndex) = generateHistogram(cgImage: cgImage) { var hasAlpha = false for i in 0 ..< 255 { if histogramBins[alphaBinIndex][i] > 0 { hasAlpha = true + break } } - guard hasAlpha else { - return false - } - var matching: Int = 0 - var total: Int = 0 - for y in 0 ..< Int(context.size.height) { - for x in 0 ..< Int(context.size.width) { - var hue: CGFloat = 0.0 - var saturation: CGFloat = 0.0 - var brightness: CGFloat = 0.0 - var alpha: CGFloat = 0.0 - context.colorAt(CGPoint(x: x, y: y)).getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) - - if alpha > 0.0 { - total += 1 - if saturation < 0.1 && brightness < 0.25 { - matching += 1 + if hasAlpha { + var matching: Int = 0 + var total: Int = 0 + for y in 0 ..< Int(context.size.height) { + for x in 0 ..< Int(context.size.width) { + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + if context.colorAt(CGPoint(x: x, y: y)).getHue(nil, saturation: &saturation, brightness: &brightness, alpha: &alpha) { + if alpha < 1.0 { + hasAlpha = true + } + + if alpha > 0.0 { + total += 1 + if saturation < 0.1 && brightness < 0.25 { + matching += 1 + } + } } } } + return CGFloat(matching) / CGFloat(total) > 0.85 } - return CGFloat(matching) / CGFloat(total) > 0.85 } return false } diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index bb099b3dc2..12288aa469 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -83,7 +83,15 @@ final class InstantPageController: ViewController { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId))) } }, navigateBack: { [weak self] in - self?.navigationController?.popViewController(animated: true) + if let strongSelf = self, let controllers = strongSelf.navigationController?.viewControllers.reversed() { + for controller in controllers { + if !(controller is InstantPageController) { + strongSelf.navigationController?.popToViewController(controller, animated: true) + return + } + } + strongSelf.navigationController?.popViewController(animated: true) + } }) self.displayNodeDidLoad() diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 8f22140be9..4ebb1d84e4 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -258,7 +258,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.setupScrollOffsetOnLayout = self.webPage == nil self.webPage = webPage - self.initialAnchor = anchor + self.initialAnchor = anchor?.removingPercentEncoding self.currentLayout = nil self.updateLayout() @@ -483,9 +483,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { itemNode = newNode if let itemNode = itemNode as? InstantPageDetailsNode { - itemNode.requestLayoutUpdate = { [weak self] in + itemNode.requestLayoutUpdate = { [weak self] animated in if let strongSelf = self { - strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: true) + strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated) } } @@ -526,9 +526,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { if self.visibleTiles[tileIndex] == nil { let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor) tileNode.frame = tileFrame -// if case let .animated(duration, _) = transition { -// tileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) -// } if let topNode = topNode { self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode) } else { @@ -793,7 +790,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { if let currentLayout = self.currentLayout { for item in currentLayout.items { - let itemFrame = self.effectiveFrameForItem(item).insetBy(dx: -2.0, dy: -2.0) + let itemFrame = self.effectiveFrameForItem(item) if itemFrame.contains(location) { if let item = item as? InstantPageTextItem, item.selectable { return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY)) @@ -843,7 +840,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.openUrl(url) + if canOpenIn { + strongSelf.openUrlIn(url) + } else { + strongSelf.openUrl(url) + } } }), ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in @@ -924,24 +925,31 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat)? { + private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, [InstantPageDetailsItem])? { for item in items { if let item = item as? InstantPageAnchorItem, item.anchor == anchor { - return (item, 0.0) + return (item, 0.0, []) } else if let item = item as? InstantPageTextItem { if let lineIndex = item.anchors[anchor] { - return (item, item.lines[lineIndex].frame.minY - 10.0) + return (item, item.lines[lineIndex].frame.minY - 10.0, []) } } else if let item = item as? InstantPageDetailsItem { - if let anchorItem = findAnchorItem(anchor, items: item.items) { - return anchorItem + if let (foundItem, offset, detailsItems) = self.findAnchorItem(anchor, items: item.items) { + var detailsItems = detailsItems + detailsItems.insert(item, at: 0) + return (foundItem, offset, detailsItems) } } } return nil } + private func presentReferenceView(item: InstantPageTextItem) { + let controller = InstantPageReferenceController(account: self.account, item: item) + self.present(controller, nil) + } + private func openUrl(_ url: InstantPageUrlItem) { guard let items = self.currentLayout?.items else { return @@ -950,15 +958,43 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { var baseUrl = url.url var anchor: String? if let anchorRange = url.url.range(of: "#") { - anchor = String(baseUrl[anchorRange.upperBound...]) + anchor = String(baseUrl[anchorRange.upperBound...]).removingPercentEncoding baseUrl = String(baseUrl[.. [InstantPageMedia] { var medias: [InstantPageMedia] = [] for item in items { @@ -1138,12 +1186,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - private func updateDetailsExpanded(_ index: Int, _ expanded: Bool) { + private func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true) { if var currentExpandedDetails = self.currentExpandedDetails { currentExpandedDetails[index] = expanded self.currentExpandedDetails = currentExpandedDetails } - self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: true) + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated) } private func presentSettings() { diff --git a/TelegramUI/InstantPageDetailsNode.swift b/TelegramUI/InstantPageDetailsNode.swift index 013ec181a6..9ad1d26ad9 100644 --- a/TelegramUI/InstantPageDetailsNode.swift +++ b/TelegramUI/InstantPageDetailsNode.swift @@ -28,7 +28,7 @@ final class InstantPageDetailsContentNode : ASDisplayNode { var currentExpandedDetails: [Int : Bool]? var currentDetailsItems: [InstantPageDetailsItem] = [] - var requestLayoutUpdate: (() -> Void)? + var requestLayoutUpdate: ((Bool) -> Void)? var currentLayout: InstantPageLayout let contentSize: CGSize @@ -201,8 +201,8 @@ final class InstantPageDetailsContentNode : ASDisplayNode { itemNode = newNode if let itemNode = itemNode as? InstantPageDetailsNode { - itemNode.requestLayoutUpdate = { [weak self] in - self?.requestLayoutUpdate?() + itemNode.requestLayoutUpdate = { [weak self] animated in + self?.requestLayoutUpdate?(animated) } } } @@ -297,12 +297,12 @@ final class InstantPageDetailsContentNode : ASDisplayNode { // } } - private func updateDetailsExpanded(_ index: Int, _ expanded: Bool) { + func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) { if var currentExpandedDetails = self.currentExpandedDetails { currentExpandedDetails[index] = expanded self.currentExpandedDetails = currentExpandedDetails } - self.requestLayoutUpdate?() + self.requestLayoutUpdate?(animated) } func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { @@ -331,7 +331,7 @@ final class InstantPageDetailsContentNode : ASDisplayNode { return contentOffset } - private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { + func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { for (_, itemNode) in self.visibleItemsWithNodes { if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { return detailsNode @@ -361,7 +361,7 @@ final class InstantPageDetailsContentNode : ASDisplayNode { return CGRect(origin: origin, size: tile.frame.size) } - private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { let layoutOrigin = item.frame.origin var origin = layoutOrigin @@ -454,7 +454,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { var previousNode: InstantPageDetailsNode? - var requestLayoutUpdate: (() -> Void)? + var requestLayoutUpdate: ((Bool) -> Void)? init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) { self.account = account @@ -522,8 +522,8 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { } } - self.contentNode.requestLayoutUpdate = { [weak self] in - self?.requestLayoutUpdate?() + self.contentNode.requestLayoutUpdate = { [weak self] animated in + self?.requestLayoutUpdate?(animated) } self.update(strings: strings, theme: theme) @@ -531,12 +531,12 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { @objc func buttonPressed() { self.setExpanded(!self.expanded, animated: true) + self.updateExpanded(expanded) } func setExpanded(_ expanded: Bool, animated: Bool) { self.expanded = expanded self.arrowNode.setOpen(expanded, animated: animated) - self.updateExpanded(expanded) } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift index f90b3ec714..94ede49faa 100644 --- a/TelegramUI/InstantPageNavigationBar.swift +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -30,7 +30,7 @@ final private class InstantPageProgressNode: ASDisplayNode { let transition: ContainedViewLayoutTransition if animated { - transition = .animated(duration: 0.5, curve: .spring) + transition = .animated(duration: 0.7, curve: .spring) } else { transition = .immediate } diff --git a/TelegramUI/InstantPagePlayableVideoItem.swift b/TelegramUI/InstantPagePlayableVideoItem.swift index 27e4ddba13..6c91434fbf 100644 --- a/TelegramUI/InstantPagePlayableVideoItem.swift +++ b/TelegramUI/InstantPagePlayableVideoItem.swift @@ -25,7 +25,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem { } func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { - return InstantPagePlayableVideoNode(account: account, webPage: self.webPage, media: self.media, interactive: self.interactive, openMedia: openMedia) + return InstantPagePlayableVideoNode(account: account, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/TelegramUI/InstantPagePlayableVideoNode.swift b/TelegramUI/InstantPagePlayableVideoNode.swift index d27b1b217c..a106754144 100644 --- a/TelegramUI/InstantPagePlayableVideoNode.swift +++ b/TelegramUI/InstantPagePlayableVideoNode.swift @@ -19,13 +19,13 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { private var localIsVisible = false - init(account: Account, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { + init(account: Account, webPage: TelegramMediaWebpage, theme: InstantPageTheme, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { self.account = account self.media = media self.interactive = interactive self.openMedia = openMedia - self.videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager!.audioSession, manager: account.telegramApplicationContext.mediaManager!.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), loopVideo: true, enableSound: false, fetchAutomatically: true), priority: .embedded, autoplay: true) + self.videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager!.audioSession, manager: account.telegramApplicationContext.mediaManager!.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor), priority: .embedded, autoplay: true) super.init() diff --git a/TelegramUI/InstantPageReferenceController.swift b/TelegramUI/InstantPageReferenceController.swift new file mode 100644 index 0000000000..7e51126495 --- /dev/null +++ b/TelegramUI/InstantPageReferenceController.swift @@ -0,0 +1,65 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +final class InstantPageReferenceController: ViewController { + private var controllerNode: InstantPageReferenceControllerNode { + return self.displayNode as! InstantPageReferenceControllerNode + } + + private var animatedIn = false + + private let account: Account + private let item: InstantPageTextItem + + init(account: Account, item: InstantPageTextItem) { + self.account = account + self.item = item + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = InstantPageReferenceControllerNode(account: self.account, item: self.item) + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.close = { [weak self] in + self?.dismiss() + } + } + + override public func loadView() { + super.loadView() + + self.statusBar.removeFromSupernode() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} diff --git a/TelegramUI/InstantPageReferenceControllerNode.swift b/TelegramUI/InstantPageReferenceControllerNode.swift new file mode 100644 index 0000000000..67d9e659d5 --- /dev/null +++ b/TelegramUI/InstantPageReferenceControllerNode.swift @@ -0,0 +1,180 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore + +class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private var presentationData: PresentationData + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let dimNode: ASDisplayNode + + private let wrappingScrollNode: ASScrollNode + private let contentContainerNode: ASDisplayNode + private let contentBackgroundNode: ASImageNode + + private let titleNode: ASTextNode + private let separatorNode: ASDisplayNode + + private let closeButton: HighlightableButtonNode + + var dismiss: (() -> Void)? + var close: (() -> Void)? + + init(account: Account, item: InstantPageTextItem) { + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + self.contentContainerNode.clipsToBounds = true + + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + self.contentBackgroundNode.image = roundedBackground + + self.titleNode = ASTextNode() + self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + + self.closeButton = HighlightableButtonNode() + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + + @objc func closeButtonPressed() { + self.close?() + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.closeButtonPressed() + } + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.closeButtonPressed() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + + let titleAreaHeight: CGFloat = 54.0 + + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left) + + let sideInset = floor((layout.size.width - width) / 2.0) + + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0) + + self.containerLayout = (layout, navigationBarHeight) + + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let titleSize = self.titleNode.measure(CGSize(width: width, height: titleAreaHeight)) + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: 15.0), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + //transition.updateFrame(node: self.closeButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + } +} diff --git a/TelegramUI/InstantPageTableItem.swift b/TelegramUI/InstantPageTableItem.swift index 4f146a45a1..61e1c9e661 100644 --- a/TelegramUI/InstantPageTableItem.swift +++ b/TelegramUI/InstantPageTableItem.swift @@ -288,12 +288,12 @@ func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: Instant var minCellWidth: CGFloat = 1.0 var maxCellWidth: CGFloat = 1.0 if let text = cell.text { - if let shortestTextItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: cellWidthLimit, offset: CGPoint(), media: media, webpage: webpage, minimizeWidth: true).0 { + if let shortestTextItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidthLimit - totalCellPadding), boundingWidth: cellWidthLimit, offset: CGPoint(), media: media, webpage: webpage, minimizeWidth: true).0 { minCellWidth = shortestTextItem.effectiveWidth() + totalCellPadding } - if let longestTextItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: cellWidthLimit, offset: CGPoint(), media: media, webpage: webpage).0 { - maxCellWidth = longestTextItem.effectiveWidth() + totalCellPadding + if let longestTextItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidthLimit - totalCellPadding), boundingWidth: cellWidthLimit, offset: CGPoint(), media: media, webpage: webpage).0 { + maxCellWidth = max(minCellWidth, longestTextItem.effectiveWidth() + totalCellPadding) } } if cell.colspan > 1 { @@ -369,7 +369,7 @@ func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: Instant let delta = minSpanWidth - minWidth for i in range { if let columnWidth = minColumnWidths[i] { - let growth = round(delta / CGFloat(range.count)) + let growth = floor(delta / CGFloat(range.count)) minColumnWidths[i] = columnWidth + growth availableWidth -= growth } @@ -441,7 +441,6 @@ func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: Instant origin.x += width } } - k += cell.colspan } else { break @@ -462,7 +461,7 @@ func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: Instant var additionalItems: [InstantPageItem] = [] var cellHeight: CGFloat? if let text = cell.text { - let (textItem, items, _) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: cellWidth - totalCellPadding, alignment: cell.alignment.textAlignment, offset: CGPoint(), media: media, webpage: webpage) + let (textItem, items, _) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidth - totalCellPadding), boundingWidth: cellWidth - totalCellPadding, alignment: cell.alignment.textAlignment, offset: CGPoint(), media: media, webpage: webpage) if let textItem = textItem { isEmptyRow = false textItem.frame = textItem.frame.offsetBy(dx: tableCellInsets.left, dy: 0.0) diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 3b0f88854e..38dc33918c 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -119,7 +119,6 @@ final class InstantPageTextItem: InstantPageItem { for i in 0 ..< self.lines.count { let line = self.lines[i] - let lineFrame = frameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) if lineFrame.maxY < upperOriginBound || lineFrame.minY > lowerOriginBound { continue @@ -131,10 +130,11 @@ final class InstantPageTextItem: InstantPageItem { if !line.markedItems.isEmpty { context.saveGState() for item in line.markedItems { + let itemFrame = item.frame.offsetBy(dx: lineFrame.minX, dy: 0.0) context.setFillColor(item.color.cgColor) let height = floor(item.frame.size.height * 2.2) - let rect = CGRect(x: item.frame.minX - 2.0, y: floor(item.frame.minY + (item.frame.height - height) / 2.0), width: item.frame.width + 4.0, height: height) + let rect = CGRect(x: itemFrame.minX - 2.0, y: floor(itemFrame.minY + (itemFrame.height - height) / 2.0), width: itemFrame.width + 4.0, height: height) let path = UIBezierPath.init(roundedRect: rect, cornerRadius: 3.0) context.addPath(path.cgPath) context.fillPath() @@ -146,7 +146,8 @@ final class InstantPageTextItem: InstantPageItem { if !line.strikethroughItems.isEmpty { for item in line.strikethroughItems { - context.fill(CGRect(x: item.frame.minX, y: item.frame.minY + floor((lineFrame.size.height / 2.0) + 1.0), width: item.frame.size.width, height: 1.0)) + let itemFrame = item.frame.offsetBy(dx: lineFrame.minX, dy: 0.0) + context.fill(CGRect(x: itemFrame.minX, y: itemFrame.minY + floor((lineFrame.size.height / 2.0) + 1.0), width: itemFrame.size.width, height: 1.0)) } } } @@ -401,7 +402,7 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem { } } -func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil) -> NSAttributedString { +func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil, boundingWidth: CGFloat? = nil) -> NSAttributedString { switch text { case .empty: return NSAttributedString(string: "", attributes: styleStack.textAttributes()) @@ -451,7 +452,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt case let .concat(texts): let string = NSMutableAttributedString() for text in texts { - let substring = attributedStringForRichText(text, styleStack: styleStack, url: url) + let substring = attributedStringForRichText(text, styleStack: styleStack, url: url, boundingWidth: boundingWidth) string.append(substring) } return string @@ -483,6 +484,11 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt let descent: CGFloat let width: CGFloat } + + var dimensions = dimensions + if let boundingWidth = boundingWidth { + dimensions = dimensions.fittedToWidthOrSmaller(boundingWidth) + } let extentBuffer = UnsafeMutablePointer.allocate(capacity: 1) extentBuffer.initialize(to: RunStruct(ascent: 0.0, descent: 0.0, width: dimensions.width)) var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in @@ -581,6 +587,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var line = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastIndex, lineCharacterCount), 100.0) var lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil)) let lineRange = NSMakeRange(lastIndex, lineCharacterCount) + let substring = string.attributedSubstring(from: lineRange).string var stop = false if maxNumberOfLines > 0 && lines.count == maxNumberOfLines - 1 && lastIndex + lineCharacterCount < string.length { @@ -634,7 +641,11 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo } } - if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 1.0, let imageItem = lineImageItems.last { + if substring.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && lineImageItems.count > 0 { + extraDescent += max(6.0, fontLineSpacing / 2.0) + } + + if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 5.0, let imageItem = lineImageItems.last { indexOffset = -(lastIndex + lineCharacterCount - imageItem.range.lowerBound) continue } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 4dd6b3eb2f..2b88d6b757 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -2559,19 +2559,40 @@ func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumA }) } - var remoteArtworkData: Signal<(Data?, Data?, Bool), NoError> = .single((nil, nil, false)) - if let albumArt = albumArt { + var immediateArtworkData: Signal<(Data?, Data?, Bool), NoError> = .single((nil, nil, false)) + + if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + let thumbnailResource = smallestRepresentation.resource + + let fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: fileReference.resourceReference(thumbnailResource)) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + immediateArtworkData = thumbnail + |> map { thumbnailData in + return (thumbnailData, nil, false) + } + } else if let albumArt = albumArt { if thumbnail { - remoteArtworkData = albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) + immediateArtworkData = albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) |> map { thumbnailData in return (thumbnailData, nil, false) } } else { - remoteArtworkData = albumArtFullSizeDatas(postbox: postbox, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) + immediateArtworkData = albumArtFullSizeDatas(postbox: postbox, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) } } - return combineLatest(fileArtworkData, remoteArtworkData) + return combineLatest(fileArtworkData, immediateArtworkData) |> map { fileArtworkData, remoteArtworkData in let remoteThumbnailData = remoteArtworkData.0 let remoteFullSizeData = remoteArtworkData.1 diff --git a/TelegramUI/SharePeersContainerNode.swift b/TelegramUI/SharePeersContainerNode.swift index 6581c2a3a6..8c084b4a7a 100644 --- a/TelegramUI/SharePeersContainerNode.swift +++ b/TelegramUI/SharePeersContainerNode.swift @@ -151,16 +151,16 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { let previousItems = Atomic<[SharePeerEntry]?>(value: []) self.disposable.set((items - |> deliverOnMainQueue).start(next: { [weak self] entries in - if let strongSelf = self { - let previousEntries = previousItems.swap(entries) - strongSelf.entries = entries - - let firstTime = previousEntries == nil - let transition = preparedGridEntryTransition(account: account, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) - strongSelf.enqueueTransition(transition, firstTime: firstTime) - } - })) + |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + let previousEntries = previousItems.swap(entries) + strongSelf.entries = entries + + let firstTime = previousEntries == nil + let transition = preparedGridEntryTransition(account: account, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)