From c5c90f76d435efcde7d6f936c02fc9c540be1d15 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 17 Nov 2018 12:32:30 +0400 Subject: [PATCH] Album artwork from ID3 tags Instant View improvements --- TelegramUI.xcodeproj/project.pbxproj | 12 +- TelegramUI/AvatarNode.swift | 2 + TelegramUI/ChatControllerInteraction.swift | 1 + TelegramUI/ChatEmptyItem.swift | 186 ----------------- TelegramUI/ChatEmptyNode.swift | 17 +- TelegramUI/ChatHistoryEntriesForView.swift | 2 - TelegramUI/ChatHistoryEntry.swift | 13 +- TelegramUI/ChatHistoryGridNode.swift | 4 +- TelegramUI/ChatHistoryListNode.swift | 4 - TelegramUI/ChatMessageStickerItemNode.swift | 11 +- TelegramUI/ChatTextInputPanelNode.swift | 4 +- TelegramUI/EditSettingsController.swift | 2 +- TelegramUI/GalleryController.swift | 2 +- TelegramUI/ID3Artwork.h | 9 + TelegramUI/ID3Artwork.m | 127 ++++++++++++ TelegramUI/ImageTransparency.swift | 54 +++-- TelegramUI/InstantPageControllerNode.swift | 37 +++- TelegramUI/InstantPageDetailsNode.swift | 22 +- TelegramUI/InstantPageGalleryController.swift | 4 + TelegramUI/InstantPageImageNode.swift | 6 +- TelegramUI/InstantPageLayout.swift | 52 ++++- TelegramUI/InstantPageLayoutSpacings.swift | 8 +- TelegramUI/InstantPageNavigationBar.swift | 24 ++- TelegramUI/InstantPagePeerReferenceItem.swift | 6 +- TelegramUI/InstantPagePeerReferenceNode.swift | 26 ++- TelegramUI/InstantPageTextItem.swift | 21 +- TelegramUI/InstantPageTextStyleStack.swift | 33 ++- TelegramUI/InstantPageTheme.swift | 18 +- TelegramUI/ItemListAvatarAndNameItem.swift | 13 +- TelegramUI/ListMessageSnippetItemNode.swift | 14 +- TelegramUI/OpenChatMessage.swift | 8 +- .../PeerMediaCollectionController.swift | 4 +- TelegramUI/PhotoResources.swift | 195 ++++++++++++------ TelegramUI/ShareControllerNode.swift | 53 +++-- TelegramUI/StringWithAppliedEntities.swift | 20 +- TelegramUI/TelegramUIPrivate/module.modulemap | 1 + TelegramUI/UrlHandling.swift | 14 ++ 37 files changed, 618 insertions(+), 411 deletions(-) delete mode 100644 TelegramUI/ChatEmptyItem.swift create mode 100644 TelegramUI/ID3Artwork.h create mode 100644 TelegramUI/ID3Artwork.m diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 1c87986010..747f6d95b7 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -57,6 +57,8 @@ 09AE3823214C110900850BFD /* LegacySecureIdScanController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */; }; 09C3466D2167D63A00B76780 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C3466C2167D63A00B76780 /* Accessibility.swift */; }; 09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */; }; + 09C9EA33219F79F600E90146 /* ID3Artwork.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C9EA31219F79F500E90146 /* ID3Artwork.m */; }; + 09C9EA34219F79F600E90146 /* ID3Artwork.h in Headers */ = {isa = PBXBuildFile; fileRef = 09C9EA32219F79F600E90146 /* ID3Artwork.h */; }; 09D304152173C0E900C00567 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304142173C0E900C00567 /* WatchManager.swift */; }; 09D304182173C15700C00567 /* WatchSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304172173C15700C00567 /* WatchSettingsController.swift */; }; 09FE756D2153F5F900A3120F /* CallRouteActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */; }; @@ -791,7 +793,6 @@ D0EC6DA31EB9F58900EBF1C3 /* ChatMessageDateHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */; }; D0EC6DA41EB9F58900EBF1C3 /* ChatMessageActionButtonsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */; }; D0EC6DA51EB9F58900EBF1C3 /* ChatBotInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */; }; - D0EC6DA61EB9F58900EBF1C3 /* ChatEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C48F431E81D5110075317D /* ChatEmptyItem.swift */; }; D0EC6DA71EB9F58900EBF1C3 /* ChatMessageBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02298361E0C34E900707F91 /* ChatMessageBackground.swift */; }; D0EC6DA81EB9F58900EBF1C3 /* ChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB471D703268005A521C /* ChatInterfaceState.swift */; }; D0EC6DA91EB9F58900EBF1C3 /* ChatPresentationInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */; }; @@ -1115,6 +1116,8 @@ 09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySecureIdScanController.swift; sourceTree = ""; }; 09C3466C2167D63A00B76780 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = ""; }; + 09C9EA31219F79F500E90146 /* ID3Artwork.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ID3Artwork.m; sourceTree = ""; }; + 09C9EA32219F79F600E90146 /* ID3Artwork.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ID3Artwork.h; sourceTree = ""; }; 09D304142173C0E900C00567 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = ""; }; 09D304172173C15700C00567 /* WatchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsController.swift; sourceTree = ""; }; 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = ""; }; @@ -1773,7 +1776,6 @@ D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoNode.swift; sourceTree = ""; }; D0C44B631FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismissGestureRecognizer.swift; sourceTree = ""; }; D0C45E9E213FFAFD00988156 /* Lottie.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Lottie.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D0C48F431E81D5110075317D /* ChatEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEmptyItem.swift; sourceTree = ""; }; D0C50DE81E93A07900F62E39 /* libtgvoip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = libtgvoip.framework; path = "../libtgvoip/build/Debug-iphoneos/libtgvoip.framework"; sourceTree = ""; }; D0C50E281E93A33700F62E39 /* VoipDynamic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VoipDynamic.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/VoipDynamic.framework"; sourceTree = ""; }; D0C50E371E93CB1500F62E39 /* NotificationContainerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationContainerController.swift; sourceTree = ""; }; @@ -4249,7 +4251,6 @@ D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */, D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */, D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */, - D0C48F431E81D5110075317D /* ChatEmptyItem.swift */, D02298361E0C34E900707F91 /* ChatMessageBackground.swift */, D0754D231EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift */, D091C7A31F8EBB1E00D7DE13 /* ChatPresentationData.swift */, @@ -4488,6 +4489,8 @@ 09C3466C2167D63A00B76780 /* Accessibility.swift */, D0068FA721760FA300D1B315 /* StoreDownloadedMedia.swift */, 0902838C2194AEB90067EFBD /* ImageTransparency.swift */, + 09C9EA32219F79F600E90146 /* ID3Artwork.h */, + 09C9EA31219F79F500E90146 /* ID3Artwork.m */, ); name = Utils; sourceTree = ""; @@ -4649,6 +4652,7 @@ D0E9BAC61F05738600F079A4 /* STPAPIClient.h in Headers */, D00ADFD91EBA2E9D00873D2E /* OngoingCallThreadLocalContext.h in Headers */, D06F31E22135829B001A0F12 /* EDSunriseSet.h in Headers */, + 09C9EA34219F79F600E90146 /* ID3Artwork.h in Headers */, D0E9BA531F0559DA00F079A4 /* STPImageLibrary+Private.h in Headers */, D0E9BA601F055A4300F079A4 /* STPDelegateProxy.h in Headers */, 096C98C121787C6700C211FF /* TGBridgeAudioDecoder.h in Headers */, @@ -5055,6 +5059,7 @@ D0EC6D191EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContext.swift in Sources */, D02D60AE206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift in Sources */, D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */, + 09C9EA33219F79F600E90146 /* ID3Artwork.m in Sources */, D0FA08CA2049BEAC00DD23FC /* ChatEmptyNode.swift in Sources */, D053DADC201AAAB100993D32 /* ChatTextInputMenu.swift in Sources */, D0EC6D1A1EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, @@ -5337,7 +5342,6 @@ D0EC6DA31EB9F58900EBF1C3 /* ChatMessageDateHeader.swift in Sources */, D0EC6DA41EB9F58900EBF1C3 /* ChatMessageActionButtonsNode.swift in Sources */, D0EC6DA51EB9F58900EBF1C3 /* ChatBotInfoItem.swift in Sources */, - D0EC6DA61EB9F58900EBF1C3 /* ChatEmptyItem.swift in Sources */, D0E9BAE41F0574D800F079A4 /* STPBankAccountParams.m in Sources */, D0E412D3206A7DC100BEE4A2 /* DateSelectionActionSheetController.swift in Sources */, D06887F01F72DEE6000AB936 /* ShareInputFieldNode.swift in Sources */, diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 95c1f132d3..ad392cb431 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -235,6 +235,8 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode = editOverlayNode } self.editOverlayNode?.isHidden = false + } else { + self.editOverlayNode?.isHidden = true } parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true) diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 023fe2dfeb..12444a7d06 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -40,6 +40,7 @@ public enum ChatControllerInteractionLongTapAction { public enum ChatControllerInteractionOpenMessageMode { case `default` case stream + case shared } public final class ChatControllerInteraction { diff --git a/TelegramUI/ChatEmptyItem.swift b/TelegramUI/ChatEmptyItem.swift deleted file mode 100644 index c556c092b6..0000000000 --- a/TelegramUI/ChatEmptyItem.swift +++ /dev/null @@ -1,186 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -private let messageFont = Font.medium(14.0) - -final class ChatEmptyItem: ListViewItem { - fileprivate let presentationData: ChatPresentationData - fileprivate let tagMask: MessageTags? - - init(presentationData: ChatPresentationData, tagMask: MessageTags?) { - self.presentationData = presentationData - self.tagMask = tagMask - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { - let configure = { - let node = ChatEmptyItemNode(rotated: self.tagMask == nil) - - let nodeLayout = node.asyncLayout() - let (layout, apply) = nodeLayout(self, params) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - completion(node, { - return (nil, { apply(.None) }) - }) - } - if Thread.isMainThread { - async { - configure() - } - } else { - configure() - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? ChatEmptyItemNode { - let nodeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = nodeLayout(self, params) - Queue.mainQueue().async { - completion(layout, { - apply(animation) - }) - } - } - } - } - } -} - -final class ChatEmptyItemNode: ListViewItemNode { - private let rotated: Bool - - var controllerInteraction: ChatControllerInteraction? - - let offsetContainer: ASDisplayNode - let backgroundNode: ASImageNode - let iconNode: ASImageNode - let textNode: TextNode - - private var theme: PresentationTheme? - - private var item: ChatEmptyItem? - - init(rotated: Bool) { - self.rotated = rotated - self.offsetContainer = ASDisplayNode() - - self.backgroundNode = ASImageNode() - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.iconNode = ASImageNode() - self.textNode = TextNode() - - super.init(layerBacked: false, dynamicBounce: true, rotated: rotated) - - if rotated { - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - } - - self.addSubnode(self.offsetContainer) - self.offsetContainer.addSubnode(self.backgroundNode) - self.offsetContainer.addSubnode(self.iconNode) - self.offsetContainer.addSubnode(self.textNode) - self.wantsTrailingItemSpaceUpdates = true - } - - func asyncLayout() -> (_ item: ChatEmptyItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let makeTextLayout = TextNode.asyncLayout(self.textNode) - let currentTheme = self.theme - return { [weak self] item, params in - self?.item = item - - let width = params.width - var updatedBackgroundImage: UIImage? - - let iconImage: UIImage? = PresentationResourcesChat.chatEmptyItemIconImage(item.presentationData.theme.theme) - - if currentTheme !== item.presentationData.theme { - updatedBackgroundImage = PresentationResourcesChat.chatEmptyItemBackgroundImage(item.presentationData.theme.theme) - } - - let attributedText: NSAttributedString - if let tagMask = item.tagMask { - let text: String - if tagMask == .photoOrVideo { - text = item.presentationData.strings.SharedMedia_EmptyText - } else if tagMask == .file { - text = item.presentationData.strings.SharedMedia_EmptyFilesText - } else { - text = "" - } - attributedText = NSAttributedString(string: text, font: messageFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor, paragraphAlignment: .center) - } else { - attributedText = NSAttributedString(string: item.presentationData.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: item.presentationData.theme.theme.chat.serviceMessage.serviceMessagePrimaryTextColor, paragraphAlignment: .center) - } - - let horizontalEdgeInset: CGFloat = 10.0 - let horizontalContentInset: CGFloat = 12.0 - let verticalItemInset: CGFloat = 10.0 - let verticalContentInset: CGFloat = 14.0 - - var imageSize = CGSize(width: 80.0, height: 80.0) - if let iconImage = iconImage { - imageSize = iconImage.size - } - let imageSpacing: CGFloat = 18.0 - - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - - let contentWidth = max(textLayout.size.width, 120.0) - - let backgroundFrame = CGRect(origin: CGPoint(x: floor((width - contentWidth - horizontalContentInset * 2.0) / 2.0), y: verticalItemInset + 4.0), size: CGSize(width: contentWidth + horizontalContentInset * 2.0, height: textLayout.size.height + imageSize.height + imageSpacing + verticalContentInset * 2.0)) - let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + horizontalContentInset + floor((contentWidth - textLayout.size.width) / 2.0), y: backgroundFrame.origin.y + verticalContentInset + imageSize.height + imageSpacing), size: textLayout.size) - let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + horizontalContentInset + floor((contentWidth - imageSize.width) / 2.0), y: backgroundFrame.origin.y + verticalContentInset), size: imageSize) - - let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height + imageSpacing + textLayout.size.height + verticalItemInset * 2.0 + verticalContentInset * 2.0 + 4.0), insets: UIEdgeInsets()) - return (itemLayout, { _ in - if let strongSelf = self { - strongSelf.theme = item.presentationData.theme.theme - - if let updatedBackgroundImage = updatedBackgroundImage { - strongSelf.backgroundNode.image = updatedBackgroundImage - } - - strongSelf.iconNode.image = iconImage - - let _ = textApply() - strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) - strongSelf.backgroundNode.frame = backgroundFrame - strongSelf.textNode.frame = textFrame - strongSelf.iconNode.frame = iconFrame - } - }) - } - } - - override func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { - if height.isLessThanOrEqualTo(0.0) { - transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) - } else { - transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.rotated ? (floor(height) / 2.0) : (-floor(height) / 4.0)), size: self.offsetContainer.bounds.size)) - } - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) - } -} diff --git a/TelegramUI/ChatEmptyNode.swift b/TelegramUI/ChatEmptyNode.swift index 0287aff3cc..404cc13e31 100644 --- a/TelegramUI/ChatEmptyNode.swift +++ b/TelegramUI/ChatEmptyNode.swift @@ -12,19 +12,16 @@ private let titleFont = Font.medium(15.0) private let messageFont = Font.regular(14.0) private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNodeContent { - private let iconNode: ASImageNode private let textNode: ImmediateTextNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? override init() { - self.iconNode = ASImageNode() self.textNode = ImmediateTextNode() super.init() - self.addSubnode(self.iconNode) self.addSubnode(self.textNode) } @@ -33,24 +30,18 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings - self.iconNode.image = PresentationResourcesChat.chatEmptyItemIconImage(interfaceState.theme) self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) } - let insets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) + let insets = UIEdgeInsets(top: 6.0, left: 10.0, bottom: 6.0, right: 10.0) - let iconVerticalInset: CGFloat = 14.0 - let iconSize = self.iconNode.image?.size ?? CGSize() let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - let spacing: CGFloat = 26.0 - let contentWidth = max(iconSize.width, textSize.width) - let contentHeight = iconVerticalInset + iconSize.height + spacing + textSize.height + let contentWidth = textSize.width + let contentHeight = textSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) - let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - iconSize.width) / 2.0), y: contentRect.minY + iconVerticalInset), size: iconSize) - transition.updateFrame(node: self.iconNode, frame: iconFrame) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: iconFrame.maxY + spacing), size: textSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: insets.top), size: textSize)) return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index 9f1e2f899a..0cc1d8694e 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -98,8 +98,6 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { entries.insert(.ChatInfoEntry(botInfo.description, presentationData), at: 0) - } else if view.entries.isEmpty && includeEmptyEntry { - //entries.insert(.EmptyChatInfoEntry(presentationData.theme, presentationData.strings, view.tagMask), at: 0) } } } else if includeSearchEntry { diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index b3a77ded27..095c0f94ad 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -29,7 +29,6 @@ enum ChatHistoryEntry: Identifiable, Comparable { case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection, Bool)], ChatPresentationData) case UnreadEntry(MessageIndex, ChatPresentationData) case ChatInfoEntry(String, ChatPresentationData) - case EmptyChatInfoEntry(ChatPresentationData, MessageTags?) case SearchEntry(PresentationTheme, PresentationStrings) var stableId: UInt64 { @@ -44,10 +43,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { return UInt64(3) << 40 case .ChatInfoEntry: return UInt64(4) << 40 - case .EmptyChatInfoEntry: - return UInt64(5) << 40 case .SearchEntry: - return UInt64(6) << 40 + return UInt64(5) << 40 } } @@ -63,8 +60,6 @@ enum ChatHistoryEntry: Identifiable, Comparable { return index case .ChatInfoEntry: return MessageIndex.absoluteLowerBound() - case .EmptyChatInfoEntry: - return MessageIndex.absoluteLowerBound() case .SearchEntry: return MessageIndex.absoluteLowerBound() } @@ -185,12 +180,6 @@ enum ChatHistoryEntry: Identifiable, Comparable { } else { return false } - case let .EmptyChatInfoEntry(lhsPresentationData, lhsTagMask): - if case let .EmptyChatInfoEntry(rhsPresentationData, rhsTagMask) = rhs, lhsPresentationData === rhsPresentationData, lhsTagMask == rhsTagMask { - return true - } else { - return false - } case let .SearchEntry(lhsTheme, lhsStrings): if case let .SearchEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index 1d6ffe989a..366854f176 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -86,7 +86,7 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt case .UnreadEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) - case .ChatInfoEntry, .EmptyChatInfoEntry, .SearchEntry: + case .ChatInfoEntry, .SearchEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) } @@ -105,7 +105,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt case .UnreadEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) - case .ChatInfoEntry, .EmptyChatInfoEntry, .SearchEntry: + case .ChatInfoEntry, .SearchEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) } diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 97f2afce73..a583c0da52 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -179,8 +179,6 @@ private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, a return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData), directionHint: entry.directionHint) case let .ChatInfoEntry(text, presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) - case let .EmptyChatInfoEntry(presentationData, tagMask): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(presentationData: presentationData, tagMask: tagMask), directionHint: entry.directionHint) case let .SearchEntry(theme, strings): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() @@ -224,8 +222,6 @@ private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, a return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData), directionHint: entry.directionHint) case let .ChatInfoEntry(text, presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) - case let .EmptyChatInfoEntry(presentationData, tagMask): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(presentationData: presentationData, tagMask: tagMask), directionHint: entry.directionHint) case let .SearchEntry(theme, strings): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 2c18785f06..cedc5818df 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -213,7 +213,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let edited = false let sentViaBot = false - let viewCount: Int? = nil + var viewCount: Int? = nil + for attribute in item.message.attributes { + if let _ = attribute as? EditedMessageAttribute { + // edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + }// else if let _ = attribute as? InlineBotMessageAttribute { + // sentViaBot = true + // } + } let dateText = stringForMessageTimestampStatus(message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, strings: item.presentationData.strings, format: .minimal) diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 857353371a..3a85cd2e9c 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -1334,8 +1334,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if isPNG && images.count == 1, let image = images.first { - if imageHasTransparency(image) { + if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage { + if imageHasTransparency(cgImage) { self.paste(.sticker(image)) return false } diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift index 6106bc16c1..351d3f5953 100644 --- a/TelegramUI/EditSettingsController.swift +++ b/TelegramUI/EditSettingsController.swift @@ -277,7 +277,7 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName let actionsDisposable = DisposableSet() let updateAvatarDisposable = MetaDisposable() - actionsDisposable.add(updateAvatarDisposable) + //actionsDisposable.add(updateAvatarDisposable) let updatePeerNameDisposable = MetaDisposable() actionsDisposable.add(updatePeerNameDisposable) diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index b6d94eff90..1c61bb99f6 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -119,7 +119,7 @@ private let italicFont = Font.italic(16.0) private let fixedFont = UIFont(name: "Menlo-Regular", size: 15.0) ?? textFont func galleryCaptionStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString { - return stringWithAppliedEntities(text, entities: entities, baseColor: .white, linkColor: .white, baseFont: textFont, linkFont: textFont, boldFont: boldFont, italicFont: italicFont, fixedFont: fixedFont) + return stringWithAppliedEntities(text, entities: entities, baseColor: .white, linkColor: UIColor(rgb: 0x5ac8fa), baseFont: textFont, linkFont: textFont, boldFont: boldFont, italicFont: italicFont, fixedFont: fixedFont, underlineLinks: false) } func galleryItemForEntry(account: Account, presentationData: PresentationData, entry: MessageHistoryEntry, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, playbackCompleted: @escaping () -> Void = {}, openUrl: @escaping (String) -> Void = { _ in }, openUrlOptions: @escaping (String) -> Void = { _ in }) -> GalleryItem? { diff --git a/TelegramUI/ID3Artwork.h b/TelegramUI/ID3Artwork.h new file mode 100644 index 0000000000..cfbb497ac1 --- /dev/null +++ b/TelegramUI/ID3Artwork.h @@ -0,0 +1,9 @@ +#ifndef Telegram_ID3Artwork_h +#define Telegram_ID3Artwork_h + +#import +#import + +NSData * _Nullable albumArtworkData(NSData * _Nonnull data); + +#endif diff --git a/TelegramUI/ID3Artwork.m b/TelegramUI/ID3Artwork.m new file mode 100644 index 0000000000..b2340f40c4 --- /dev/null +++ b/TelegramUI/ID3Artwork.m @@ -0,0 +1,127 @@ +#import "ID3Artwork.h" + +const uint8_t ID3v2[5] = {0x49, 0x44, 0x33, 0x02, 0x00}; +const uint8_t ID3v3[5] = {0x49, 0x44, 0x33, 0x03, 0x00}; +const NSUInteger ID3VersionOffset = 3; +const NSUInteger ID3SizeOffset = 6; +const NSUInteger ID3TagOffset = 10; +const NSUInteger ID3ArtOffset = 12; + +const NSUInteger ID3v2FrameOffset = 6; +const NSUInteger ID3v3FrameOffset = 10; + +const uint8_t ID3v2Artwork[3] = {0x50, 0x49, 0x43}; +const uint8_t ID3v3Artwork[4] = {0x41, 0x50, 0x49, 0x43}; + +const uint8_t JPGMagic[3] = {0xff, 0xd8, 0xff}; +const uint8_t PNGMagic[4] = {0x89, 0x50, 0x4e, 0x47}; + +uint32_t getSize(const uint8_t *bytes) { + uint32_t size = CFSwapInt32HostToBig(*(const uint32_t *)(bytes + ID3SizeOffset)); + uint32_t b1 = (size & 0x7F000000) >> 3; + uint32_t b2 = (size & 0x007F0000) >> 2; + uint32_t b3 = (size & 0x00007F00) >> 1; + uint32_t b4 = size & 0x0000007F; + return b1 + b2 + b3 + b4; +} + +uint32_t frameOffsetForVersion(uint8_t version) { + return version == 2 ? ID3v2FrameOffset : ID3v3FrameOffset; +} + +uint32_t frameSizeForBytes(const uint8_t *framePtr, uint8_t version) { + uint8_t offset = version == 2 ? 2 : 4; + uint32_t size = CFSwapInt32HostToBig(*(uint32_t *)(framePtr + offset)); + + if (version == 2) { + size &= 0x00FFFFFF; + } + + return size + frameOffsetForVersion(version); +} + +bool isArtworkFrame(const uint8_t *framePtr, uint8_t version) { + if (version == 2) { + return memcmp(framePtr, ID3v2Artwork, 3) == 0; + } + + return memcmp(framePtr, ID3v3Artwork, 4) == 0; +} + +NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { + if (data.length < 4) { + return nil; + } + + const uint8_t *bytes = data.bytes; + + if (!(memcmp(bytes, ID3v2, 5) == 0 || memcmp(bytes, ID3v3, 5) == 0)) { + return nil; + } + + uint8_t version = bytes[ID3VersionOffset]; + uint32_t size = CFSwapInt32HostToBig(*(const uint32_t *)(bytes + ID3SizeOffset)); + uint32_t b1 = (size & 0x7F000000) >> 3; + uint32_t b2 = (size & 0x007F0000) >> 2; + uint32_t b3 = (size & 0x00007F00) >> 1; + uint32_t b4 = size & 0x0000007F; + size = b1 + b2 + b3 + b4; + + const uint8_t *ptr = data.bytes + ID3TagOffset; + + uint32_t pos = 0; + while (pos < size) { + const uint8_t * const frameBytes = ptr + pos; + uint32_t frameSize = frameSizeForBytes(frameBytes, version); + + if (isArtworkFrame(frameBytes, version)) { + uint32_t frameOffset = frameOffsetForVersion(version); + const uint8_t *ptr = frameBytes + frameOffset; + + bool isJpg = false; + uint32_t imageOffset = UINT32_MAX; + for (uint32_t i = 0; i < frameSize - 4; i++) { + if (memcmp(ptr + i, JPGMagic, 3) == 0) { + imageOffset = i; + isJpg = true; + break; + } else if (memcmp(ptr + i, PNGMagic, 4) == 0) { + imageOffset = i; + break; + } + } + + if (imageOffset != UINT32_MAX) { + if (isJpg) { + NSMutableData *jpgData = [[NSMutableData alloc] initWithCapacity:frameSize + 1024]; + uint8_t previousByte = 0xff; + uint32_t skippedBytes = 0; + + for (NSUInteger i = 0; i < frameSize - imageOffset + skippedBytes; i++) { + uint8_t byte = (uint8_t)ptr[imageOffset + i]; + if (byte == 0x00 && previousByte == 0xff) { + skippedBytes++; + } else { + [jpgData appendBytes:&byte length:1]; + } + if (byte == 0xd9 && previousByte == 0xff) { + break; + } + previousByte = byte; + } + return jpgData; + } + else { + return [[NSData alloc] initWithBytes:ptr + imageOffset length:frameSize - imageOffset]; + } + } + } + else if (frameBytes[0] == 0x00 && frameBytes[1] == 0x00 && frameBytes[2] == 0x00) { + break; + } + + pos += frameSize; + } + + return nil; +} diff --git a/TelegramUI/ImageTransparency.swift b/TelegramUI/ImageTransparency.swift index 517797a080..8fd2cab2f3 100644 --- a/TelegramUI/ImageTransparency.swift +++ b/TelegramUI/ImageTransparency.swift @@ -1,15 +1,7 @@ import UIKit import Accelerate -func imageHasTransparency(_ image: UIImage) -> Bool { - guard let cgImage = image.cgImage, cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { - return false - } - let alphaInfo = cgImage.alphaInfo - guard alphaInfo == .first || alphaInfo == .last || alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast else { - return false - } - +private func generateHistogram(cgImage: CGImage) -> ([[vImagePixelCount]], Int)? { var sourceBuffer = vImage_Buffer() defer { free(sourceBuffer.data) @@ -29,9 +21,9 @@ func imageHasTransparency(_ image: UIImage) -> Bool { var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &cgImageFormat, nil, cgImage, noFlags) assert(error == kvImageNoError) - if alphaInfo == .premultipliedLast { + if cgImage.alphaInfo == .premultipliedLast { error = vImageUnpremultiplyData_RGBA8888(&sourceBuffer, &sourceBuffer, noFlags) - } else if alphaInfo == .premultipliedFirst { + } else if cgImage.alphaInfo == .premultipliedFirst { error = vImageUnpremultiplyData_ARGB8888(&sourceBuffer, &sourceBuffer, noFlags) } assert(error == kvImageNoError) @@ -45,11 +37,43 @@ func imageHasTransparency(_ image: UIImage) -> Bool { error = vImageHistogramCalculation_ARGB8888(&sourceBuffer, &mutableHistogram, noFlags) assert(error == kvImageNoError) - let alphaBinIndex = alphaInfo == .last || alphaInfo == .premultipliedLast ? 3 : 0 - for i in 0 ..< 255 { - if histogramBins[alphaBinIndex][i] > 0 { - return true + let alphaBinIndex = [.last, .premultipliedLast].contains(cgImage.alphaInfo) ? 3 : 0 + return (histogramBins, alphaBinIndex) +} + +func imageHasTransparency(_ cgImage: CGImage) -> Bool { + guard cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { + return false + } + guard [.first, .last, .premultipliedFirst, .premultipliedLast].contains(cgImage.alphaInfo) else { + return false + } + if let (histogramBins, alphaBinIndex) = generateHistogram(cgImage: cgImage) { + for i in 0 ..< 255 { + if histogramBins[alphaBinIndex][i] > 0 { + return true + } } } return false } + +func imageIsMonochrome(_ cgImage: CGImage) -> Bool { + guard cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { + return false + } + if let (histogramBins, alphaBinIndex) = generateHistogram(cgImage: cgImage) { + + } + +// SSE, bias = 0, [0,0,0] +// if adjust_color_bias: +// bias = ImageStat.Stat(thumb).mean[:3] +// bias = [b - sum(bias)/3 for b in bias ] +// for pixel in thumb.getdata(): +// mu = sum(pixel)/3 +// SSE += sum((pixel[i] - mu - bias[i])*(pixel[i] - mu - bias[i]) for i in [0,1,2]) + + + return false +} diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 641c62fd80..4ef0fca5a5 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -204,7 +204,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } - break + if !(item is InstantPageImageItem || item is InstantPagePlayableVideoItem) { + break + } } } } @@ -654,8 +656,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.statusBar.verticalOffset = 0.0 } + var title: String? + if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + title = content.websiteName + } + transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame) - self.navigationBar.updateLayout(size: navigationBarFrame.size, minHeight: minBarHeight, maxHeight: maxBarHeight, topInset: containerLayout.safeInsets.top, leftInset: containerLayout.safeInsets.left, rightInset: containerLayout.safeInsets.right, pageProgress: pageProgress, transition: transition) + self.navigationBar.updateLayout(size: navigationBarFrame.size, minHeight: minBarHeight, maxHeight: maxBarHeight, topInset: containerLayout.safeInsets.top, leftInset: containerLayout.safeInsets.left, rightInset: containerLayout.safeInsets.right, title: title, pageProgress: pageProgress, transition: transition) transition.animateView { self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationBarFrame.size.height, left: 0.0, bottom: containerLayout.intrinsicInsets.bottom, right: 0.0) @@ -690,7 +697,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: UIColor(rgb: 0x007be8).withAlphaComponent(0.4)) + let highlightColor = self.theme?.linkHighlightColor ?? UIColor(rgb: 0x007ee5).withAlphaComponent(0.4) + linkHighlightingNode = LinkHighlightingNode(color: highlightColor) linkHighlightingNode.isUserInteractionEnabled = false self.linkHighlightingNode = linkHighlightingNode self.scrollNode.addSubnode(linkHighlightingNode) @@ -716,13 +724,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return contentOffset } - private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { for (_, itemNode) in self.visibleItemsWithNodes { if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { - return CGSize(width: item.frame.width, height: detailsNode.effectiveContentSize.height + item.titleHeight) + return detailsNode } } - return item.frame.size + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let node = nodeForDetailsItem(item) { + return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } } private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { @@ -925,11 +941,14 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let page = content.instantPage, page.url == baseUrl, let anchor = anchor { if !anchor.isEmpty { - if let (anchorItem, offset) = findAnchorItem(String(anchor), items: items) { - self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: anchorItem.frame.minY + offset - self.scrollNode.view.contentInset.top), animated: true) - return + if let (item, offset) = findAnchorItem(String(anchor), items: items) { + let frame = effectiveFrameForItem(item) + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: frame.minY + offset - self.scrollNode.view.contentInset.top), animated: true) } + } else { + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top), animated: true) } + return } var cancelImpl: (() -> Void)? diff --git a/TelegramUI/InstantPageDetailsNode.swift b/TelegramUI/InstantPageDetailsNode.swift index 48cbf8f0e3..04b21c3ec4 100644 --- a/TelegramUI/InstantPageDetailsNode.swift +++ b/TelegramUI/InstantPageDetailsNode.swift @@ -178,7 +178,6 @@ final class InstantPageDetailsContentNode : ASDisplayNode { if itemNode == nil { let itemIndex = itemIndex - let embedIndex = embedIndex let detailsIndex = detailsIndex if let newNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in self?.openMedia(media) @@ -187,15 +186,11 @@ final class InstantPageDetailsContentNode : ASDisplayNode { }, openUrl: { [weak self] url in self?.openUrl(url) }, updateWebEmbedHeight: { [weak self] height in - //self?.updateWebEmbedHeight(embedIndex, height) }, updateDetailsExpanded: { [weak self] expanded in self?.updateDetailsExpanded(detailsIndex, expanded) }, currentExpandedDetails: self.currentExpandedDetails) { newNode.frame = itemFrame newNode.updateLayout(size: itemFrame.size, transition: transition) -// if case let .animated(duration, _) = transition { -// newNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) -// } if let topNode = topNode { self.insertSubnode(newNode, aboveSubnode: topNode) } else { @@ -240,9 +235,6 @@ final class InstantPageDetailsContentNode : ASDisplayNode { 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.insertSubnode(tileNode, aboveSubnode: topNode) } else { @@ -339,13 +331,21 @@ final class InstantPageDetailsContentNode : ASDisplayNode { return contentOffset } - private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { for (_, itemNode) in self.visibleItemsWithNodes { if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { - return CGSize(width: item.frame.width, height: detailsNode.effectiveContentSize.height + item.titleHeight) + return detailsNode } } - return item.frame.size + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let node = nodeForDetailsItem(item) { + return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } } private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { diff --git a/TelegramUI/InstantPageGalleryController.swift b/TelegramUI/InstantPageGalleryController.swift index d899daadf1..41b0a9713d 100644 --- a/TelegramUI/InstantPageGalleryController.swift +++ b/TelegramUI/InstantPageGalleryController.swift @@ -36,6 +36,8 @@ struct InstantPageGalleryEntry: Equatable { styleStack.push(.fontSize(16.0)) styleStack.push(.textColor(.white)) styleStack.push(.markerColor(UIColor(rgb: 0x313131))) + styleStack.push(.linkColor(UIColor(rgb: 0x5ac8fa))) + styleStack.push(.linkMarkerColor(UIColor(rgb: 0x5ac8fa, alpha: 0.2))) styleStack.push(.fontSerif(false)) if let url = self.media.url { @@ -59,6 +61,8 @@ struct InstantPageGalleryEntry: Equatable { styleStack.push(.fontSize(14.0)) styleStack.push(.textColor(.white)) styleStack.push(.markerColor(UIColor(rgb: 0x313131))) + styleStack.push(.linkColor(UIColor(rgb: 0x5ac8fa))) + styleStack.push(.linkMarkerColor(UIColor(rgb: 0x5ac8fa, alpha: 0.2))) styleStack.push(.fontSerif(false)) //styleStack.push(.lineSpacingFactor(1.0)) credit = attributedStringForRichText(mediaCredit, styleStack: styleStack) diff --git a/TelegramUI/InstantPageImageNode.swift b/TelegramUI/InstantPageImageNode.swift index 502f513f01..538ce47114 100644 --- a/TelegramUI/InstantPageImageNode.swift +++ b/TelegramUI/InstantPageImageNode.swift @@ -58,7 +58,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) if file.mimeType.hasPrefix("image/") { _ = freeMediaFileInteractiveFetched(account: account, fileReference: fileReference).start() - self.imageNode.setSignal(chatMessageImageFile(account: account, fileReference: fileReference, thumbnail: false, fetched: true)) + self.imageNode.setSignal(instantPageImageFile(account: account, fileReference: fileReference, fetched: true)) } else { self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, videoReference: fileReference)) } @@ -76,6 +76,10 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height)) self.imageNode.setSignal(chatMapSnapshotImage(account: account, resource: resource)) + } else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image { + let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: imageReference, storeToDownloadsPeerType: nil).start()) } } diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 01f55158a1..9ed0d19ed5 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -28,6 +28,8 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP let attributes = theme.textCategories.attributes(type: category, link: link) stack.push(.textColor(attributes.color)) stack.push(.markerColor(theme.markerColor)) + stack.push(.linkColor(theme.linkColor)) + stack.push(.linkMarkerColor(theme.linkHighlightColor)) switch attributes.font.style { case .sans: stack.push(.fontSerif(false)) @@ -232,7 +234,11 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) - switch item { + var effectiveItem = item + if case let .blocks(blocks, num) = effectiveItem, blocks.isEmpty { + effectiveItem = .text(.plain(" "), num) + } + switch effectiveItem { case let .text(text, _): let (textItem, textItems, textItemSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, offset: CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height), media: media, webpage: webpage) @@ -259,14 +265,14 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins listItems.append(contentsOf: textItems) case let .blocks(blocks, _): var previousBlock: InstantPageBlock? - var originY: CGFloat = 0.0 + var originY: CGFloat = contentSize.height for subBlock in blocks { let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights) let spacing: CGFloat = previousBlock != nil ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0 let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing)) if previousBlock == nil { - originY = contentSize.height + spacing + originY += spacing } listItems.append(contentsOf: blockItems) contentSize.height += subLayout.contentSize.height + spacing @@ -605,10 +611,20 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins } var items: [InstantPageItem] = [] - let item = InstantPageWebEmbedItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size), url: url, html: html, enableScrolling: allowScrolling) + var contentSize: CGSize + let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) + let item: InstantPageItem + if let url = url, let coverId = coverId, let image = media[coverId] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { + let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: size, duration: nil, author: nil, image: image, file: nil, instantPage: nil) + let content = TelegramMediaWebpageContent.Loaded(loadedContent) + + item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: content), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false) + + } else { + item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling) + } items.append(item) - - var contentSize = item.frame.size + contentSize = item.frame.size let (captionItems, captionSize) = layoutCaption(caption, contentSize) items.append(contentsOf: captionItems) @@ -619,17 +635,31 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins var contentSize = CGSize(width: boundingWidth, height: 0.0) var items: [InstantPageItem] = [] + var offset: CGFloat = 0.0 + var previousItemHasRTL = false - if let previousItem = previousItems.last as? InstantPageTextItem, previousItem.containsRTL { - previousItemHasRTL = true + if let previousItem = previousItems.last as? InstantPageTextItem { + if previousItem.containsRTL { + previousItemHasRTL = true + } + var minY = previousItem.frame.minY + if let firstItem = previousItems.first { + minY = firstItem.frame.maxY + } + offset = minY - previousItem.frame.maxY + } + if !offset.isZero { + offset -= 40.0 + 14.0 } if let peer = peer { - let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, safeInset: safeInset, rtl: rtl || previousItemHasRTL) + let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL) items.append(item) - contentSize.height += 40.0 + if offset.isZero { + contentSize.height += 40.0 + } } - return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + return InstantPageLayout(origin: CGPoint(x: 0.0, y: offset), contentSize: contentSize, items: items) case let .anchor(name): let item = InstantPageAnchorItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 0.0)), anchor: name) return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) diff --git a/TelegramUI/InstantPageLayoutSpacings.swift b/TelegramUI/InstantPageLayoutSpacings.swift index 58da90e193..3c7c49995c 100644 --- a/TelegramUI/InstantPageLayoutSpacings.swift +++ b/TelegramUI/InstantPageLayoutSpacings.swift @@ -10,7 +10,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> return 25.0 case (_, .blockQuote), (.blockQuote, _), (_, .pullQuote), (.pullQuote, _): return 27.0 - case (.kicker, .title): + case (.kicker, .title), (.cover, .title): return 16.0 case (_, .title): return 20.0 @@ -49,16 +49,16 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> } } else if let lower = lower { switch lower { - case .cover, .channelBanner, .details, .anchor: + case .cover, .channelBanner, .details: return 0.0 default: - return 24.0 + return 25.0 } } else { if let upper = upper, case .relatedArticles = upper { return 0.0 } else { - return 24.0 + return 25.0 } } } diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift index 08dc31fad8..0335efcfc1 100644 --- a/TelegramUI/InstantPageNavigationBar.swift +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -17,6 +17,8 @@ final class InstantPageNavigationBar: ASDisplayNode { private let arrowNode: ASImageNode private let titleNode: ASTextNode + private let progressNode: ASDisplayNode + private let intrinsicMoreSize: CGSize private let intrinsicSmallMoreSize: CGSize private let intrinsicActionSize: CGSize @@ -25,6 +27,8 @@ final class InstantPageNavigationBar: ASDisplayNode { private var dimmed: Bool = false private var buttonsAlphaFactor: CGFloat = 1.0 + private var currentTitle: String? + var back: (() -> Void)? var share: (() -> Void)? var settings: (() -> Void)? @@ -59,6 +63,10 @@ final class InstantPageNavigationBar: ASDisplayNode { self.arrowNode.displaysAsynchronously = false self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + + self.progressNode = ASDisplayNode() + self.progressNode.backgroundColor = .white super.init() @@ -72,6 +80,7 @@ final class InstantPageNavigationBar: ASDisplayNode { self.addSubnode(self.moreButton) self.addSubnode(self.actionButton) self.addSubnode(self.titleNode) + self.addSubnode(self.progressNode) self.backButton.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) @@ -107,7 +116,7 @@ final class InstantPageNavigationBar: ASDisplayNode { } } - func updateLayout(size: CGSize, minHeight: CGFloat, maxHeight: CGFloat, topInset: CGFloat, leftInset: CGFloat, rightInset: CGFloat, pageProgress: CGFloat, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, minHeight: CGFloat, maxHeight: CGFloat, topInset: CGFloat, leftInset: CGFloat, rightInset: CGFloat, title: String?, pageProgress: CGFloat, transition: ContainedViewLayoutTransition) { let progressHeight: CGFloat if !topInset.isZero { progressHeight = size.height - topInset + 11.0 @@ -149,10 +158,23 @@ final class InstantPageNavigationBar: ASDisplayNode { alphaFactor *= 0.5 } + if title != self.currentTitle { + self.currentTitle = title + if let title = title { + self.titleNode.transform = CATransform3DIdentity + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 44.0 - 88.0, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) / 2.0, y: size.height - 30.0), size: titleSize) + } + } + let maxMoreOffset = self.intrinsicMoreSize.height / 2.0 + floor((44.0 - self.intrinsicMoreSize.height) / 2.0) let minMoreOffset = self.intrinsicSmallMoreSize.height / 2.0 + floor((20.0 - self.intrinsicSmallMoreSize.height) / 2.0) let moreOffset = (transitionFactor * maxMoreOffset) + ((1.0 - transitionFactor) * minMoreOffset) + transition.updateTransformScale(node: self.titleNode, scale: 0.75 + transitionFactor * 0.25) + transition.updatePosition(node: self.titleNode, position: CGPoint(x: size.width / 2.0, y: size.height - moreOffset)) + transition.updateTransformScale(node: self.moreButton, scale: buttonScaleFactor) transition.updatePosition(node: self.moreButton, position: CGPoint(x: size.width - rightInset - buttonScaleFactor * self.intrinsicMoreSize.width / 2.0, y: size.height - moreOffset)) transition.updateAlpha(node: self.moreButton, alpha: alphaFactor) diff --git a/TelegramUI/InstantPagePeerReferenceItem.swift b/TelegramUI/InstantPagePeerReferenceItem.swift index a19d194b3f..f13b92b7ff 100644 --- a/TelegramUI/InstantPagePeerReferenceItem.swift +++ b/TelegramUI/InstantPagePeerReferenceItem.swift @@ -11,17 +11,19 @@ final class InstantPagePeerReferenceItem: InstantPageItem { let initialPeer: Peer let safeInset: CGFloat + let transparent: Bool let rtl: Bool - init(frame: CGRect, initialPeer: Peer, safeInset: CGFloat, rtl: Bool) { + init(frame: CGRect, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool) { self.frame = frame self.initialPeer = initialPeer self.safeInset = safeInset + self.transparent = transparent self.rtl = rtl } 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 InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, rtl: self.rtl, openPeer: openPeer) + return InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, transparent: self.transparent, rtl: self.rtl, openPeer: openPeer) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/TelegramUI/InstantPagePeerReferenceNode.swift b/TelegramUI/InstantPagePeerReferenceNode.swift index c9d749f300..45da8f5404 100644 --- a/TelegramUI/InstantPagePeerReferenceNode.swift +++ b/TelegramUI/InstantPagePeerReferenceNode.swift @@ -45,6 +45,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { private let account: Account let initialPeer: Peer let safeInset: CGFloat + private let transparent: Bool private let rtl: Bool private var strings: PresentationStrings private var theme: InstantPageTheme @@ -64,18 +65,18 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { private var joinState: JoinState = .none - init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, initialPeer: Peer, safeInset: CGFloat, rtl: Bool, openPeer: @escaping (PeerId) -> Void) { + init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (PeerId) -> Void) { self.account = account self.strings = strings self.theme = theme self.initialPeer = initialPeer self.safeInset = safeInset + self.transparent = transparent self.rtl = rtl self.openPeer = openPeer self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true - self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor self.highlightedBackgroundNode.alpha = 0.0 self.buttonNode = HighlightableButtonNode() @@ -97,7 +98,14 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { super.init() - self.backgroundColor = theme.panelBackgroundColor + if self.transparent { + self.backgroundColor = UIColor(white: 0.0, alpha: 0.6) + self.highlightedBackgroundNode.backgroundColor = UIColor(white: 1.0, alpha: 0.1) + } else { + self.backgroundColor = theme.panelBackgroundColor + self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor + } + self.addSubnode(self.highlightedBackgroundNode) self.addSubnode(self.buttonNode) self.addSubnode(self.joinNode) @@ -152,7 +160,8 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self { - strongSelf.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: strongSelf.theme.panelPrimaryColor) + let textColor = strongSelf.transparent ? UIColor.white : strongSelf.theme.panelPrimaryColor + strongSelf.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: textColor) if let peer = peer as? TelegramChannel { var joinState = strongSelf.joinState if case .member = peer.participationStatus { @@ -195,12 +204,15 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { private func applyThemeAndStrings(themeUpdated: Bool) { if let peer = self.peer { - self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.panelPrimaryColor) + let textColor = self.transparent ? UIColor.white : self.theme.panelPrimaryColor + self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: textColor) } - self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: self.theme.panelAccentColor), for: []) + let accentColor = self.transparent ? UIColor.white : self.theme.panelAccentColor + self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: accentColor), for: []) if themeUpdated { - self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: self.theme.panelSecondaryColor) + let secondaryColor = self.transparent ? UIColor.white : self.theme.panelSecondaryColor + self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: secondaryColor) self.activityIndicator.type = .custom(self.theme.panelAccentColor, 22.0, 2.0, false) } self.setNeedsLayout() diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 380b4cbffc..3a015ea3a9 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -214,7 +214,7 @@ final class InstantPageTextItem: InstantPageItem { if let (index, dict) = self.attributesAtPoint(point) { if let _ = dict[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] { if let rects = self.attributeRects(name: NSAttributedStringKey(rawValue: TelegramTextAttributes.URL), at: index) { - return rects + return rects.map { $0.insetBy(dx: 2.0, dy: -3.0) } } } } @@ -360,7 +360,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt styleStack.pop() return result case let .url(text, url, webpageId): - styleStack.push(.underline) + styleStack.push(.link(webpageId != nil)) let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: url, webpageId: webpageId)) styleStack.pop() return result @@ -503,7 +503,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let lineRange = NSMakeRange(lastIndex, lineCharacterCount) var stop = false - if maxNumberOfLines > 0 && lines.count == maxNumberOfLines - 1 { + if maxNumberOfLines > 0 && lines.count == maxNumberOfLines - 1 && lastIndex + lineCharacterCount < string.length { let attributes = string.attributes(at: lastIndex + lineCharacterCount - 1, effectiveRange: nil) if let truncateString = CFAttributedStringCreate(nil, "\u{2026}" as CFString, attributes as CFDictionary) { let truncateToken = CTLineCreateWithAttributedString(truncateString) @@ -525,11 +525,20 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) strikethroughItems.append(InstantPageTextStrikethroughItem(frame: CGRect(x: currentLineOrigin.x + lowerX, y: currentLineOrigin.y, width: upperX - lowerX, height: fontLineHeight))) - } else if let item = attributes[NSAttributedStringKey.init(rawValue: InstantPageMarkerColorAttribute)] as? UIColor { + } + if let color = attributes[NSAttributedStringKey.init(rawValue: InstantPageMarkerColorAttribute)] as? UIColor { + var lineHeight = fontLineHeight + var delta: CGFloat = 0.0 + + if let offset = attributes[NSAttributedStringKey.baselineOffset] as? CGFloat { + lineHeight = floorToScreenPixels(lineHeight * 0.85) + delta = offset * 0.6 + } let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) - markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: currentLineOrigin.x + lowerX, y: currentLineOrigin.y, width: upperX - lowerX, height: fontLineHeight), color: item)) - } else if let item = attributes[NSAttributedStringKey.init(rawValue: InstantPageAnchorAttribute)] as? String { + markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: currentLineOrigin.x + lowerX, y: currentLineOrigin.y + delta, width: upperX - lowerX, height: lineHeight), color: color)) + } + if let item = attributes[NSAttributedStringKey.init(rawValue: InstantPageAnchorAttribute)] as? String { anchorItems.append(InstantPageTextAnchorItem(name: item)) } } diff --git a/TelegramUI/InstantPageTextStyleStack.swift b/TelegramUI/InstantPageTextStyleStack.swift index 05a0764c47..dae914fab5 100644 --- a/TelegramUI/InstantPageTextStyleStack.swift +++ b/TelegramUI/InstantPageTextStyleStack.swift @@ -17,6 +17,9 @@ enum InstantPageTextStyle { case markerColor(UIColor) case marker case anchor(String) + case linkColor(UIColor) + case linkMarkerColor(UIColor) + case link(Bool) } let InstantPageLineSpacingFactorAttribute = "LineSpacingFactorAttribute" @@ -51,6 +54,9 @@ final class InstantPageTextStyleStack { var markerColor: UIColor? var marker: Bool? var anchor: String? + var linkColor: UIColor? + var linkMarkerColor: UIColor? + var link: Bool? for item in self.items.reversed() { switch item { @@ -111,6 +117,18 @@ final class InstantPageTextStyleStack { if anchor == nil { anchor = name } + case let .linkColor(color): + if linkColor == nil { + linkColor = color + } + case let .linkMarkerColor(color): + if linkMarkerColor == nil { + linkMarkerColor = color + } + case let .link(instant): + if link == nil { + link = instant + } } } @@ -134,7 +152,7 @@ final class InstantPageTextStyleStack { } else if fontFixed != nil && fontFixed! { attributes[NSAttributedStringKey.font] = UIFont(name: "Menlo-BoldItalic", size: parsedFontSize) } else { - attributes[NSAttributedStringKey.font] = Font.bold(parsedFontSize) + attributes[NSAttributedStringKey.font] = Font.semiboldItalic(parsedFontSize) } } else if bold != nil && bold! { if fontSerif != nil && fontSerif! { @@ -170,10 +188,17 @@ final class InstantPageTextStyleStack { attributes[NSAttributedStringKey.underlineStyle] = NSUnderlineStyle.styleSingle.rawValue as NSNumber } - if let color = color { - attributes[NSAttributedStringKey.foregroundColor] = color + if let link = link, let linkColor = linkColor { + attributes[NSAttributedStringKey.foregroundColor] = linkColor + if link, let linkMarkerColor = linkMarkerColor { + attributes[NSAttributedStringKey(rawValue: InstantPageMarkerColorAttribute)] = linkMarkerColor + } } else { - attributes[NSAttributedStringKey.foregroundColor] = UIColor.black + if let color = color { + attributes[NSAttributedStringKey.foregroundColor] = color + } else { + attributes[NSAttributedStringKey.foregroundColor] = UIColor.black + } } if let lineSpacingFactor = lineSpacingFactor { diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift index 5d5dc83f1e..01839c70ce 100644 --- a/TelegramUI/InstantPageTheme.swift +++ b/TelegramUI/InstantPageTheme.swift @@ -87,6 +87,7 @@ final class InstantPageTheme { let codeBlockBackgroundColor: UIColor + let linkColor: UIColor let textHighlightColor: UIColor let linkHighlightColor: UIColor let markerColor: UIColor @@ -104,11 +105,12 @@ final class InstantPageTheme { let imageEmptyColor: UIColor? - init(pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, serif: Bool, codeBlockBackgroundColor: UIColor, textHighlightColor: UIColor, linkHighlightColor: UIColor, markerColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor, tableBorderColor: UIColor, tableHeaderColor: UIColor, controlColor: UIColor, imageEmptyColor: UIColor?) { + init(pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, serif: Bool, codeBlockBackgroundColor: UIColor, linkColor: UIColor, textHighlightColor: UIColor, linkHighlightColor: UIColor, markerColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor, tableBorderColor: UIColor, tableHeaderColor: UIColor, controlColor: UIColor, imageEmptyColor: UIColor?) { self.pageBackgroundColor = pageBackgroundColor self.textCategories = textCategories self.serif = serif self.codeBlockBackgroundColor = codeBlockBackgroundColor + self.linkColor = linkColor self.textHighlightColor = textHighlightColor self.linkHighlightColor = linkHighlightColor self.markerColor = markerColor @@ -124,7 +126,7 @@ final class InstantPageTheme { } func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme { - return InstantPageTheme(pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageEmptyColor: imageEmptyColor) + return InstantPageTheme(pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageEmptyColor: imageEmptyColor) } } @@ -142,8 +144,9 @@ private let lightTheme = InstantPageTheme( ), serif: false, codeBlockBackgroundColor: UIColor(rgb: 0xf5f8fc), + linkColor: UIColor(rgb: 0x007ee5), textHighlightColor: UIColor(rgb: 0, alpha: 0.12), - linkHighlightColor: UIColor(rgb: 0, alpha: 0.12), + linkHighlightColor: UIColor(rgb: 0x007ee5, alpha: 0.07), markerColor: UIColor(rgb: 0xfef3bc), panelBackgroundColor: UIColor(rgb: 0xf3f4f5), panelHighlightedBackgroundColor: UIColor(rgb: 0xe7e7e7), @@ -170,8 +173,9 @@ private let sepiaTheme = InstantPageTheme( ), serif: false, codeBlockBackgroundColor: UIColor(rgb: 0xefe7d6), + linkColor: UIColor(rgb: 0xd19600), textHighlightColor: UIColor(rgb: 0, alpha: 0.1), - linkHighlightColor: UIColor(rgb: 0, alpha: 0.1), + linkHighlightColor: UIColor(rgb: 0xd19600, alpha: 0.1), markerColor: UIColor(rgb: 0xe5ddcd), panelBackgroundColor: UIColor(rgb: 0xefe7d6), panelHighlightedBackgroundColor: UIColor(rgb: 0xe3dccb), @@ -198,8 +202,9 @@ private let grayTheme = InstantPageTheme( ), serif: false, codeBlockBackgroundColor: UIColor(rgb: 0x555556), + linkColor: UIColor(rgb: 0x5ac8fa), textHighlightColor: UIColor(rgb: 0, alpha: 0.16), - linkHighlightColor: UIColor(rgb: 0, alpha: 0.16), + linkHighlightColor: UIColor(rgb: 0x5ac8fa, alpha: 0.13), markerColor: UIColor(rgb: 0x4b4b4b), panelBackgroundColor: UIColor(rgb: 0x555556), panelHighlightedBackgroundColor: UIColor(rgb: 0x505051), @@ -226,8 +231,9 @@ private let darkTheme = InstantPageTheme( ), serif: false, codeBlockBackgroundColor: UIColor(rgb: 0x131313), + linkColor: UIColor(rgb: 0x5ac8fa), textHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), - linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), + linkHighlightColor: UIColor(rgb: 0x5ac8fa, alpha: 0.2), markerColor: UIColor(rgb: 0x313131), panelBackgroundColor: UIColor(rgb: 0x131313), panelHighlightedBackgroundColor: UIColor(rgb: 0x1f1f1f), diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 528eb99095..310ca13899 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -5,7 +5,7 @@ import Postbox import TelegramCore import SwiftSignalKit -private let updatingAvatarOverlayImage = generateFilledCircleImage(diameter: 66.0, color: UIColor(white: 1.0, alpha: 0.5), backgroundColor: nil) +private let updatingAvatarOverlayImage = generateFilledCircleImage(diameter: 66.0, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) enum ItemListAvatarAndNameInfoItemTitleType { case group @@ -254,6 +254,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite private let avatarNode: AvatarNode private let updatingAvatarOverlay: ASImageNode + private let activityIndicator: ActivityIndicator private let callButton: HighlightableButtonNode @@ -304,6 +305,9 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite self.updatingAvatarOverlay.displayWithoutProcessing = true self.updatingAvatarOverlay.displaysAsynchronously = false + self.activityIndicator = ActivityIndicator(type: .custom(.white, 24.0, 1.0, false)) + self.activityIndicator.isHidden = true + self.nameNode = TextNode() self.nameNode.isUserInteractionEnabled = false self.nameNode.contentMode = .left @@ -324,6 +328,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.avatarNode) + self.addSubnode(self.activityIndicator) + self.addSubnode(self.nameNode) self.addSubnode(self.statusNode) @@ -531,6 +537,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite if strongSelf.updatingAvatarOverlay.supernode == nil { strongSelf.insertSubnode(strongSelf.updatingAvatarOverlay, aboveSubnode: strongSelf.avatarNode) } + strongSelf.activityIndicator.isHidden = false } else if strongSelf.updatingAvatarOverlay.supernode != nil { if animated { strongSelf.updatingAvatarOverlay.alpha = 0.0 @@ -542,6 +549,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite } else { strongSelf.updatingAvatarOverlay.removeFromSupernode() } + strongSelf.activityIndicator.isHidden = true } if item.call != nil { @@ -621,6 +629,9 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite strongSelf.avatarNode.frame = avatarFrame strongSelf.updatingAvatarOverlay.frame = avatarFrame + let indicatorSize = strongSelf.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(avatarFrame.midX - indicatorSize.width / 2.0), y: floorToScreenPixels(avatarFrame.midY - indicatorSize.height / 2.0)), size: indicatorSize) + let nameY: CGFloat if statusText.isEmpty { nameY = floor((layout.contentSize.height - nameNodeLayout.size.height) / 2.0) diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index b9184da995..6a645d2b3a 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -476,13 +476,19 @@ final class ListMessageSnippetItemNode: ListMessageNode { func activateMedia() { if let item = self.item, let currentPrimaryUrl = self.currentPrimaryUrl { - if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { - if websiteType(of: content) == .instagram { - if !item.controllerInteraction.openMessage(item.message, .default) { + if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if content.instantPage != nil { + if websiteType(of: content) == .instagram { + if !item.controllerInteraction.openMessage(item.message, .default) { + item.controllerInteraction.openInstantPage(item.message) + } + } else { item.controllerInteraction.openInstantPage(item.message) } } else { - item.controllerInteraction.openInstantPage(item.message) + if isTelegramMeLink(content.url) || !item.controllerInteraction.openMessage(item.message, .default) { + item.controllerInteraction.openUrl(currentPrimaryUrl, false, false) + } } } else { if !item.controllerInteraction.openMessage(item.message, .default) { diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index 172a40df27..ee0baf5071 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -20,7 +20,7 @@ private enum ChatMessageGalleryControllerData { case chatAvatars(AvatarGalleryController, Media) } -private func chatMessageGalleryControllerData(account: Account, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, stream: Bool, synchronousLoad: Bool) -> ChatMessageGalleryControllerData? { +private func chatMessageGalleryControllerData(account: Account, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, stream: Bool, synchronousLoad: Bool, excludeWebPageMedia: Bool = false) -> ChatMessageGalleryControllerData? { var galleryMedia: Media? var otherMedia: Media? var instantPageMedia: (TelegramMediaWebpage, [InstantPageGalleryEntry])? @@ -42,7 +42,7 @@ private func chatMessageGalleryControllerData(account: Account, message: Message galleryMedia = file } else if let image = media as? TelegramMediaImage { galleryMedia = image - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, !excludeWebPageMedia { if let file = content.file { galleryMedia = file } else if let image = content.image { @@ -151,8 +151,8 @@ func chatMessagePreviewControllerData(account: Account, message: Message, standa return nil } -func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, stream: Bool = false, navigationController: NavigationController?, modal: Bool = false, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, () -> UIView?)?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, sendSticker: ((FileMediaReference) -> Void)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void, chatAvatarHiddenMedia: @escaping (Signal, Media) -> Void) -> Bool { - if let mediaData = chatMessageGalleryControllerData(account: account, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, stream: stream, synchronousLoad: false) { +func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, stream: Bool = false, excludeWebPageMedia: Bool = false, navigationController: NavigationController?, modal: Bool = false, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, () -> UIView?)?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, sendSticker: ((FileMediaReference) -> Void)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void, chatAvatarHiddenMedia: @escaping (Signal, Media) -> Void) -> Bool { + if let mediaData = chatMessageGalleryControllerData(account: account, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, stream: stream, synchronousLoad: false, excludeWebPageMedia: excludeWebPageMedia) { switch mediaData { case let .url(url): openUrl(url) diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 0677f0ee3a..70fc0dcc08 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -78,13 +78,13 @@ public class PeerMediaCollectionController: TelegramController { } }) - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, _ in + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, mode in if let strongSelf = self, strongSelf.isNodeLoaded, let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id) { guard let navigationController = strongSelf.navigationController as? NavigationController else { return false } strongSelf.mediaCollectionDisplayNode.view.endEditing(true) - return openChatMessage(account: account, message: galleryMessage.message, standalone: false, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { + return openChatMessage(account: account, message: galleryMessage.message, standalone: false, reverseMessageGalleryOrder: true, excludeWebPageMedia: mode == .shared, navigationController: navigationController, dismissInput: { self?.mediaCollectionDisplayNode.view.endEditing(true) }, present: { c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index cd29ea5f8f..2c65f40366 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -1811,28 +1811,69 @@ func chatSecretMessageVideo(account: Account, videoReference: FileMediaReference } } +private func orientationFromExif(orientation: Int) -> UIImageOrientation { + switch orientation { + case 1: + return .up; + case 3: + return .down; + case 8: + return .left; + case 6: + return .right; + case 2: + return .upMirrored; + case 4: + return .downMirrored; + case 5: + return .leftMirrored; + case 7: + return .rightMirrored; + default: + return .up + } +} + func imageOrientationFromSource(_ source: CGImageSource) -> UIImageOrientation { if let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) { let dict = properties as NSDictionary - if let value = dict.object(forKey: "Orientation") as? NSNumber { - return UIImageOrientation(rawValue: value.intValue) ?? .up + if let value = dict.object(forKey: kCGImagePropertyOrientation) as? NSNumber { + return orientationFromExif(orientation: value.intValue) } } return .up } +private func rotationFor(_ orientation: UIImageOrientation) -> CGFloat { + switch orientation { + case .left: + return CGFloat.pi / 2.0 + case .right: + return -CGFloat.pi / 2.0 + case .down: + return -CGFloat.pi + default: + return 0.0 + } +} + func drawImage(context: CGContext, image: CGImage, orientation: UIImageOrientation, in rect: CGRect) { var restore = true var drawRect = rect switch orientation { + case .left: + fallthrough case .right: + fallthrough + case .down: + let angle = rotationFor(orientation) context.saveGState() context.translateBy(x: rect.midX, y: rect.midY) - context.rotate(by: -CGFloat.pi / 2.0) + context.rotate(by: angle) context.translateBy(x: -rect.midX, y: -rect.midY) var t = CGAffineTransform(translationX: rect.midX, y: rect.midY) - t = t.rotated(by: -CGFloat.pi / 2.0) + t = t.rotated(by: angle) t = t.translatedBy(x: -rect.midX, y: -rect.midY) drawRect = rect.applying(t) @@ -1949,6 +1990,51 @@ func chatMessageImageFile(account: Account, fileReference: FileMediaReference, t } } +func instantPageImageFile(account: Account, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) + |> map { (thumbnailData, fullSizePath, fullSizeComplete) in + return { arguments in + assertNotOnMainThread() + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + + var fullSizeImage: CGImage? + var imageOrientation: UIImageOrientation = .up + if let fullSizePath = fullSizePath { + if fullSizeComplete { + let options = NSMutableDictionary() + options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizePath) as CFURL, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { + imageOrientation = imageOrientationFromSource(imageSource) + fullSizeImage = image + } + } + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + if var fullSizeImage = fullSizeImage { +// if true || imageIsMonochrome(fullSizeImage), let tintedImage = generateTintedImage(image: UIImage(cgImage: fullSizeImage), color: .white)?.cgImage { +// fullSizeImage = tintedImage +// } + + c.setBlendMode(.normal) + c.interpolationQuality = .medium + drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} + private func avatarGalleryPhotoDatas(account: Account, representations: [(TelegramMediaImageRepresentation, MediaResourceReference)], autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.0 })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.0 })), let smallestIndex = representations.index(where: { $0.0 == smallestRepresentation }), let largestIndex = representations.index(where: { $0.0 == largestRepresentation }) { let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) @@ -2425,80 +2511,59 @@ private func drawAlbumArtPlaceholder(into c: CGContext, arguments: TransformImag } func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - - var fileThumbnailResource: TelegramMediaResource? - if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - fileThumbnailResource = smallestRepresentation.resource + var fileArtworkData: Signal = .single(nil) + if let fileReference = fileReference, let size = fileReference.media.resource.size { + fileArtworkData = fileArtworkData + |> then(postbox.mediaBox.resourceData(fileReference.media.resource, size: size, in: 0 ..< min(size, 1024 * 256)) + |> mapToSignal { data -> Signal in + return .single(albumArtworkData(data)) + }) } + var remoteArtworkData: Signal<(Data?, Data?, Bool), NoError> = .single((nil, nil, false)) if let albumArt = albumArt { if thumbnail { - return albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) |> map { thumbnailData in - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) - - var sourceImage: UIImage? - if let thumbnailData = thumbnailData, let image = UIImage(data: thumbnailData) { - sourceImage = image - } - - if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { - let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) - context.withFlippedContext { c in - c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) - } - } else { - context.withFlippedContext { c in - drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) - } - } - - addCorners(context, arguments: arguments) - - return context - } + remoteArtworkData = albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) + |> map { thumbnailData in + return (thumbnailData, nil, false) } } else { - return albumArtFullSizeDatas(postbox: postbox, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) - - var sourceImage: UIImage? - if fullSizeComplete, let fullSizeData = fullSizeData, let image = UIImage(data: fullSizeData) { - sourceImage = image - } else if let thumbnailData = thumbnailData, let image = UIImage(data: thumbnailData) { - sourceImage = image - } - - if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { - let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) - context.withFlippedContext { c in - c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) - } - } else { - context.withFlippedContext { c in - drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) - } - } - - addCorners(context, arguments: arguments) - - return context - } - } + remoteArtworkData = albumArtFullSizeDatas(postbox: postbox, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) } - } else { - return .single({ arguments in + } + + return combineLatest(fileArtworkData, remoteArtworkData) + |> map { fileArtworkData, remoteArtworkData in + let remoteThumbnailData = remoteArtworkData.0 + let remoteFullSizeData = remoteArtworkData.1 + let remoteFullSizeComplete = remoteArtworkData.2 + return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) - - context.withFlippedContext { c in - drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) + + var sourceImage: UIImage? + if let fileArtworkData = fileArtworkData, let image = UIImage(data: fileArtworkData) { + sourceImage = image + } else if remoteFullSizeComplete, let fullSizeData = remoteFullSizeData, let image = UIImage(data: fullSizeData) { + sourceImage = image + } else if let thumbnailData = remoteThumbnailData, let image = UIImage(data: thumbnailData) { + sourceImage = image + } + + if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { + let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) + context.withFlippedContext { c in + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) + } + } else { + context.withFlippedContext { c in + drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) + } } addCorners(context, arguments: arguments) return context - }) + } } } diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index db64b910aa..73a871b6b9 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -165,27 +165,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.peersContentNode?.updateFoundPeers() } - func updateActionNodesAlpha(_ nodes: [ASDisplayNode], alpha: CGFloat) { - for node in nodes { - if !node.alpha.isEqual(to: alpha) { - let previousAlpha = node.alpha - node.alpha = alpha - node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32) - - if let inputNode = node as? ShareInputFieldNode, alpha.isZero { - inputNode.deactivateInput() - } - } - } - } - - let actionNodes: [ASDisplayNode] - if strongSelf.defaultAction == nil { - actionNodes = [strongSelf.inputFieldNode, strongSelf.actionsBackgroundNode, strongSelf.actionButtonNode, strongSelf.actionSeparatorNode] - } else { - actionNodes = [strongSelf.inputFieldNode] - } - updateActionNodesAlpha(actionNodes, alpha: strongSelf.controllerInteraction!.selectedPeers.isEmpty ? 0.0 : 1.0) + strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty, inputField: true, actions: strongSelf.defaultAction == nil) strongSelf.updateButton() @@ -251,6 +231,31 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + func setActionNodesHidden(_ hidden: Bool, inputField: Bool = false, actions: Bool = false) { + func updateActionNodesAlpha(_ nodes: [ASDisplayNode], alpha: CGFloat) { + for node in nodes { + if !node.alpha.isEqual(to: alpha) { + let previousAlpha = node.alpha + node.alpha = alpha + node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32) + + if let inputNode = node as? ShareInputFieldNode, alpha.isZero { + inputNode.deactivateInput() + } + } + } + } + + var actionNodes: [ASDisplayNode] = [] + if inputField { + actionNodes.append(self.inputFieldNode) + } + if actions { + actionNodes.append(contentsOf: [self.actionsBackgroundNode, self.actionButtonNode, self.actionSeparatorNode]) + } + updateActionNodesAlpha(actionNodes, alpha: hidden ? 0.0 : 1.0) + } + func transitionToContentNode(_ contentNode: (ASDisplayNode & ShareContentContainerNode)?, fastOut: Bool = false) { if self.contentNode !== contentNode { let transition: ContainedViewLayoutTransition @@ -298,6 +303,12 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate contentNode.activate() previous.deactivate() + + if contentNode is ShareSearchContainerNode { + self.setActionNodesHidden(true, inputField: true, actions: true) + } else if !(contentNode is ShareLoadingContainerNode) { + self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty, actions: true) + } } else { if let contentNode = self.contentNode { contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index 3b6f6ef4aa..6ae3c54d99 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -39,7 +39,7 @@ func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageT } -func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, fixedFont: UIFont) -> NSAttributedString { +func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, fixedFont: UIFont, underlineLinks: Bool = true) -> NSAttributedString { var nsString: NSString? let string = NSMutableAttributedString(string: text, attributes: [NSAttributedStringKey.font: baseFont, NSAttributedStringKey.foregroundColor: baseColor]) var skipEntity = false @@ -65,7 +65,9 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba switch entity.type { case .Url: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) - string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + if underlineLinks { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } if nsString == nil { nsString = text as NSString } @@ -75,7 +77,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } - if underlineAllLinks { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.URL), value: "mailto:\(nsString!.substring(with: range))", range: range) @@ -84,7 +86,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } - if underlineAllLinks { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.URL), value: "tel:\(nsString!.substring(with: range))", range: range) @@ -93,7 +95,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } - if underlineAllLinks { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.URL), value: url, range: range) @@ -103,7 +105,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(NSAttributedStringKey.font, value: italicFont, range: range) case .Mention: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) - if linkColor.isEqual(baseColor) { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } if linkFont !== baseFont { @@ -115,7 +117,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range) case let .TextMention(peerId): string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) - if linkColor.isEqual(baseColor) { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } if linkFont !== baseFont { @@ -146,14 +148,14 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba } if !skipEntity { string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) - if linkColor.isEqual(baseColor) { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) } case .BotCommand: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) - if linkColor.isEqual(baseColor) { + if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } if nsString == nil { diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index 4f3fe4bea8..ac0f82cc19 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -30,4 +30,5 @@ module TelegramUIPrivateModule { header "../EDSunriseSet.h" header "../TGBridgeAudioDecoder.h" header "../TGBridgeAudioEncoder.h" + header "../ID3Artwork.h" } diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index c836f0b85c..cb7d34e508 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -161,6 +161,20 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig } } +func isTelegramMeLink(_ url: String) -> Bool { + let schemes = ["http://", "https://", ""] + let baseTelegramMePaths = ["telegram.me", "t.me"] + for basePath in baseTelegramMePaths { + for scheme in schemes { + let basePrefix = scheme + basePath + "/" + if url.lowercased().hasPrefix(basePrefix) { + return true + } + } + } + return false +} + func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? { let schemes = ["http://", "https://", ""] let baseTelegramMePaths = ["telegram.me", "t.me"]