diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 758ebe4a12..1abcf5f686 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -78,8 +78,7 @@ 096C98C021787C6700C211FF /* TGBridgeAudioEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */; }; 096C98C121787C6700C211FF /* TGBridgeAudioDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 096C98BD21787C6700C211FF /* TGBridgeAudioDecoder.h */; }; 096C98C221787C6700C211FF /* TGBridgeAudioDecoder.mm in Sources */ = {isa = PBXBuildFile; fileRef = 096C98BE21787C6700C211FF /* TGBridgeAudioDecoder.mm */; }; - 09749BC321F0DFFD008FDDE9 /* StickersChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09749BC221F0DFFD008FDDE9 /* StickersChatInputContextPanelNode.swift */; }; - 09749BC521F0E024008FDDE9 /* StickersChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09749BC421F0E024008FDDE9 /* StickersChatInputPanelItem.swift */; }; + 09749BC521F0E024008FDDE9 /* StickersChatInputContextPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09749BC421F0E024008FDDE9 /* StickersChatInputContextPanelItem.swift */; }; 09749BC921F1BBA1008FDDE9 /* CallFeedbackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09749BC821F1BBA1008FDDE9 /* CallFeedbackController.swift */; }; 09749BCD21F23139008FDDE9 /* WallpaperGalleryDecorationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09749BCC21F23139008FDDE9 /* WallpaperGalleryDecorationNode.swift */; }; 09749BCF21F236F2008FDDE9 /* ModernCheckNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09749BCE21F236F2008FDDE9 /* ModernCheckNode.swift */; }; @@ -989,8 +988,7 @@ D0EC6DCB1EB9F58900EBF1C3 /* ChatMediaInputTrendingPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0575AEC1E9FF1AD006F2541 /* ChatMediaInputTrendingPane.swift */; }; D0EC6DCC1EB9F58900EBF1C3 /* ChatButtonKeyboardInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */; }; D0EC6DCD1EB9F58900EBF1C3 /* ChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */; }; - D0EC6DCE1EB9F58900EBF1C3 /* HorizontalStickersChatContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */; }; - D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */; }; + D0EC6DCE1EB9F58900EBF1C3 /* StickersChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE11E447AD500A2CD3A /* StickersChatInputContextPanelNode.swift */; }; D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */; }; D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */; }; D0EC6DD21EB9F58900EBF1C3 /* MentionChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */; }; @@ -1283,8 +1281,7 @@ 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioEncoder.h; sourceTree = ""; }; 096C98BD21787C6700C211FF /* TGBridgeAudioDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioDecoder.h; sourceTree = ""; }; 096C98BE21787C6700C211FF /* TGBridgeAudioDecoder.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGBridgeAudioDecoder.mm; sourceTree = ""; }; - 09749BC221F0DFFD008FDDE9 /* StickersChatInputContextPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersChatInputContextPanelNode.swift; sourceTree = ""; }; - 09749BC421F0E024008FDDE9 /* StickersChatInputPanelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersChatInputPanelItem.swift; sourceTree = ""; }; + 09749BC421F0E024008FDDE9 /* StickersChatInputContextPanelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersChatInputContextPanelItem.swift; sourceTree = ""; }; 09749BC821F1BBA1008FDDE9 /* CallFeedbackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallFeedbackController.swift; sourceTree = ""; }; 09749BCC21F23139008FDDE9 /* WallpaperGalleryDecorationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperGalleryDecorationNode.swift; sourceTree = ""; }; 09749BCE21F236F2008FDDE9 /* ModernCheckNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernCheckNode.swift; sourceTree = ""; }; @@ -1679,8 +1676,7 @@ D048EA8A1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsThemeItemNode.swift; sourceTree = ""; }; D048EA8C1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsSwitchItemNode.swift; sourceTree = ""; }; D048EA8E1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemNode.swift; sourceTree = ""; }; - D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.swift; sourceTree = ""; }; - D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickerGridItem.swift; sourceTree = ""; }; + D049EAE11E447AD500A2CD3A /* StickersChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickersChatInputContextPanelNode.swift; sourceTree = ""; }; D049EAE51E44AD5600A2CD3A /* ChatMediaInputMetaSectionItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputMetaSectionItemNode.swift; sourceTree = ""; }; D049EAED1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListRecentPeersListItem.swift; sourceTree = ""; }; D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceController.swift; sourceTree = ""; }; @@ -3196,10 +3192,8 @@ D049EAE01E447AB700A2CD3A /* Stickers */ = { isa = PBXGroup; children = ( - D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */, - D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */, - 09749BC221F0DFFD008FDDE9 /* StickersChatInputContextPanelNode.swift */, - 09749BC421F0E024008FDDE9 /* StickersChatInputPanelItem.swift */, + D049EAE11E447AD500A2CD3A /* StickersChatInputContextPanelNode.swift */, + 09749BC421F0E024008FDDE9 /* StickersChatInputContextPanelItem.swift */, ); name = Stickers; sourceTree = ""; @@ -5458,7 +5452,6 @@ 092F36902157AB46001A9F49 /* ItemListCallListItem.swift in Sources */, D0EC6CC61EB9F58800EBF1C3 /* PresenceStrings.swift in Sources */, D0EC6CC71EB9F58800EBF1C3 /* PeerNotificationSoundStrings.swift in Sources */, - 09749BC321F0DFFD008FDDE9 /* StickersChatInputContextPanelNode.swift in Sources */, D01C06C01FBF118A001561AB /* MessageUtils.swift in Sources */, D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */, D0EC6CC81EB9F58800EBF1C3 /* ProgressiveImage.swift in Sources */, @@ -6057,10 +6050,9 @@ D0EC6DCD1EB9F58900EBF1C3 /* ChatInputContextPanelNode.swift in Sources */, D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */, D0EEE9A12165585F001292A6 /* DocumentPreviewController.swift in Sources */, - D0EC6DCE1EB9F58900EBF1C3 /* HorizontalStickersChatContextPanelNode.swift in Sources */, + D0EC6DCE1EB9F58900EBF1C3 /* StickersChatInputContextPanelNode.swift in Sources */, D0BCC3D2203F0A6C008126C2 /* StringForMessageTimestampStatus.swift in Sources */, - 09749BC521F0E024008FDDE9 /* StickersChatInputPanelItem.swift in Sources */, - D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */, + 09749BC521F0E024008FDDE9 /* StickersChatInputContextPanelItem.swift in Sources */, D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */, 09B4EE5621A8149C00847FA6 /* ItemListInfoItem.swift in Sources */, D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */, diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index d5000dd9d3..604e012362 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -77,11 +77,11 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa switch inputQueryResult { case let .stickers(results): if !results.isEmpty { - if let currentPanel = currentPanel as? HorizontalStickersChatContextPanelNode { + if let currentPanel = currentPanel as? StickersChatInputContextPanelNode { currentPanel.updateResults(results.map({ $0.file })) return currentPanel } else { - let panel = HorizontalStickersChatContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = StickersChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.controllerInteraction = controllerInteraction panel.interfaceInteraction = interfaceInteraction panel.updateResults(results.map({ $0.file })) diff --git a/TelegramUI/ChatMessageActionUrlAuthController.swift b/TelegramUI/ChatMessageActionUrlAuthController.swift index a116416da0..047019843e 100644 --- a/TelegramUI/ChatMessageActionUrlAuthController.swift +++ b/TelegramUI/ChatMessageActionUrlAuthController.swift @@ -224,16 +224,17 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var size = size size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) self.validLayout = size var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let titleSize = self.titleNode.measure(size) + let titleSize = self.titleNode.measure(measureSize) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) origin.y += titleSize.height + 9.0 - let textSize = self.textNode.measure(size) + let textSize = self.textNode.measure(measureSize) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) origin.y += textSize.height + 16.0 diff --git a/TelegramUI/ChatTextInputAttributes.swift b/TelegramUI/ChatTextInputAttributes.swift index e83d23c3b4..da8b39e461 100644 --- a/TelegramUI/ChatTextInputAttributes.swift +++ b/TelegramUI/ChatTextInputAttributes.swift @@ -656,8 +656,6 @@ func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttributedStri let substring = string.substring(with: match.range(at: 1)) + text + string.substring(with: match.range(at: 5)) result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.monospace: true as NSNumber])) - //newText.append() - //attributes.append(.pre(matchIndex + match.range(at: 1).length ..< matchIndex + match.range(at: 1).length + text.length)) offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6)) } @@ -690,7 +688,7 @@ func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttributedStri } if string.length > 0 { - result.append(text.attributedSubstring(from: NSMakeRange(stringOffset, string.length - stringOffset))) + result.append(text.attributedSubstring(from: NSMakeRange(text.length - string.length, string.length))) } return result diff --git a/TelegramUI/EmojisChatInputContextPanelNode.swift b/TelegramUI/EmojisChatInputContextPanelNode.swift index d90deef63a..3ca48e6059 100644 --- a/TelegramUI/EmojisChatInputContextPanelNode.swift +++ b/TelegramUI/EmojisChatInputContextPanelNode.swift @@ -6,7 +6,6 @@ import Display private struct EmojisChatInputContextPanelEntryStableId: Hashable, Equatable { let symbol: String - let text: String } private struct EmojisChatInputContextPanelEntry: Comparable, Identifiable { @@ -16,7 +15,7 @@ private struct EmojisChatInputContextPanelEntry: Comparable, Identifiable { let text: String var stableId: EmojisChatInputContextPanelEntryStableId { - return EmojisChatInputContextPanelEntryStableId(symbol: self.symbol, text: self.text) + return EmojisChatInputContextPanelEntryStableId(symbol: self.symbol) } func withUpdatedTheme(_ theme: PresentationTheme) -> EmojisChatInputContextPanelEntry { diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift index 4a09ccb6f8..860ac978e7 100644 --- a/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -94,7 +94,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { entries.append(entry) index += 1 } - self.prepareTransition(from: self.currentEntries ?? [], to: entries) + self.prepareTransition(from: self.currentEntries, to: entries) } private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { diff --git a/TelegramUI/HorizontalStickerGridItem.swift b/TelegramUI/HorizontalStickerGridItem.swift deleted file mode 100644 index a219ae0c2e..0000000000 --- a/TelegramUI/HorizontalStickerGridItem.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import Display -import TelegramCore -import SwiftSignalKit -import AsyncDisplayKit -import Postbox - -final class HorizontalStickerGridItem: GridItem { - let account: Account - let file: TelegramMediaFile - let stickersInteraction: HorizontalStickersChatContextPanelInteraction - let interfaceInteraction: ChatPanelInterfaceInteraction - - let section: GridSection? = nil - - init(account: Account, file: TelegramMediaFile, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { - self.account = account - self.file = file - self.stickersInteraction = stickersInteraction - self.interfaceInteraction = interfaceInteraction - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = HorizontalStickerGridItemNode() - node.setup(account: self.account, item: self) - node.interfaceInteraction = self.interfaceInteraction - return node - } - - func update(node: GridItemNode) { - guard let node = node as? HorizontalStickerGridItemNode else { - assertionFailure() - return - } - node.setup(account: self.account, item: self) - node.interfaceInteraction = self.interfaceInteraction - } -} - -final class HorizontalStickerGridItemNode: GridItemNode { - private var currentState: (Account, HorizontalStickerGridItem, CGSize)? - private let imageNode: TransformImageNode - - private let stickerFetchedDisposable = MetaDisposable() - - var interfaceInteraction: ChatPanelInterfaceInteraction? - - private var currentIsPreviewing: Bool = false - - var stickerItem: StickerPackItem? { - if let (_, item, _) = self.currentState { - return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.file, indexKeys: []) - } else { - return nil - } - } - - override init() { - self.imageNode = TransformImageNode() - - super.init() - - self.addSubnode(self.imageNode) - } - - deinit { - stickerFetchedDisposable.dispose() - } - - override func didLoad() { - super.didLoad() - - self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) - } - - func setup(account: Account, item: HorizontalStickerGridItem) { - if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.file.id != item.file.id { - if let dimensions = item.file.dimensions { - self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start()) - - self.currentState = (account, item, dimensions) - self.setNeedsLayout() - } - } - - self.updatePreviewing(animated: false) - } - - override func layout() { - super.layout() - - let bounds = self.bounds - let boundingSize = bounds.insetBy(dx: 2.0, dy: 2.0).size - - if let (_, _, mediaDimensions) = self.currentState { - let imageSize = mediaDimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: CGSize(width: imageSize.width, height: imageSize.height)) - self.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: imageSize.width, height: imageSize.height)) - self.imageNode.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) - } - } - - @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { - if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - interfaceInteraction.sendSticker(.standalone(media: item.file)) - } - } - - func transitionNode() -> ASDisplayNode? { - return self.imageNode - } - - func updatePreviewing(animated: Bool) { - var isPreviewing = false - if let (_, item, _) = self.currentState { - isPreviewing = item.stickersInteraction.previewedStickerItem == self.stickerItem - } - if self.currentIsPreviewing != isPreviewing { - self.currentIsPreviewing = isPreviewing - - if isPreviewing { - self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0) - if animated { - self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4) - } - } else { - self.layer.sublayerTransform = CATransform3DIdentity - if animated { - self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5) - } - } - } - } -} diff --git a/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/TelegramUI/HorizontalStickersChatContextPanelNode.swift deleted file mode 100644 index cce0e68bf6..0000000000 --- a/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ /dev/null @@ -1,280 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Postbox -import TelegramCore -import Display -import SwiftSignalKit - -final class HorizontalStickersChatContextPanelInteraction { - var previewedStickerItem: StickerPackItem? -} - -private struct StickerEntry: Identifiable, Comparable { - let index: Int - let file: TelegramMediaFile - - var stableId: MediaId { - return self.file.fileId - } - - static func ==(lhs: StickerEntry, rhs: StickerEntry) -> Bool { - return lhs.index == rhs.index && lhs.stableId == rhs.stableId - } - - static func <(lhs: StickerEntry, rhs: StickerEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(account: Account, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem { - return HorizontalStickerGridItem(account: account, file: self.file, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction) - } -} - -private struct StickerEntryTransition { - let deletions: [Int] - let insertions: [GridNodeInsertItem] - let updates: [GridNodeUpdateItem] - let updateFirstIndexInSectionOffset: Int? - let stationaryItems: GridNodeStationaryItems - let scrollToItem: GridNodeScrollToItem? -} - -private func preparedGridEntryTransition(account: Account, from fromEntries: [StickerEntry], to toEntries: [StickerEntry], stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> StickerEntryTransition { - let stationaryItems: GridNodeStationaryItems = .none - let scrollToItem: GridNodeScrollToItem? = nil - - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction)) } - - return StickerEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem) -} - -final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { - private var strings: PresentationStrings - - private let gridNode: GridNode - private let backgroundNode: ASDisplayNode - - private var validLayout: (CGSize, CGFloat, CGFloat, ChatPresentationInterfaceState)? - private var currentEntries: [StickerEntry]? - private var queuedTransitions: [(StickerEntryTransition, Bool)] = [] - - public var controllerInteraction: ChatControllerInteraction? - private let stickersInteraction: HorizontalStickersChatContextPanelInteraction - - private var stickerPreviewController: StickerPreviewController? - - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { - self.strings = strings - - self.gridNode = GridNode() - self.gridNode.view.disablesInteractiveTransitionGestureRecognizer = true - self.gridNode.scrollView.alwaysBounceVertical = true - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor - - self.stickersInteraction = HorizontalStickersChatContextPanelInteraction() - - super.init(context: context, theme: theme, strings: strings) - - self.placement = .overTextInput - self.isOpaque = false - self.clipsToBounds = true - - self.addSubnode(self.gridNode) - self.gridNode.addSubnode(self.backgroundNode) - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in - if let strongSelf = self { - let convertedPoint = strongSelf.gridNode.view.convert(point, from: strongSelf.view) - guard strongSelf.gridNode.bounds.contains(convertedPoint) else { - return nil - } - - if let itemNode = strongSelf.gridNode.itemNodeAtPoint(strongSelf.view.convert(point, to: strongSelf.gridNode.view)) as? HorizontalStickerGridItemNode, let item = itemNode.stickerItem { - return strongSelf.context.account.postbox.transaction { transaction -> Bool in - return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) - } - |> deliverOnMainQueue - |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in - if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - var menuItems: [PeekControllerMenuItem] = [] - menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { - controllerInteraction.sendSticker(.standalone(media: item.file), true) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { - if let strongSelf = self { - if isStarred { - let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() - } else { - let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() - } - } - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { - if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - loop: for attribute in item.file.attributes { - switch attribute { - case let .Sticker(_, packReference, _): - if let packReference = packReference { - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: controllerInteraction.navigationController()) - controller.sendSticker = { file in - if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.sendSticker(file, true) - } - } - - controllerInteraction.navigationController()?.view.window?.endEditing(true) - controllerInteraction.presentController(controller, nil) - } - break loop - default: - break - } - } - } - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {}) - ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) - } else { - return nil - } - } - } - } - return nil - }, present: { [weak self] content, sourceNode in - if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { - return sourceNode - }) - strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) - return controller - } - return nil - }, updateContent: { [weak self] content in - if let strongSelf = self { - var item: StickerPackItem? - if let content = content as? StickerPreviewPeekContent, case let .pack(contentItem) = content.item { - item = contentItem - } - strongSelf.updatePreviewingItem(item: item, animated: true) - } - })) - } - - func updateResults(_ results: [TelegramMediaFile]) { - let firstTime = self.currentEntries == nil - let previousEntries = self.currentEntries ?? [] - var entries: [StickerEntry] = [] - for i in 0 ..< results.count { - entries.append(StickerEntry(index: i, file: results[i])) - } - self.currentEntries = entries - - if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate, interfaceState: validLayout.3) - } - - let transition = preparedGridEntryTransition(account: self.context.account, from: previousEntries, to: entries, stickersInteraction: self.stickersInteraction, interfaceInteraction: self.interfaceInteraction!) - self.enqueueTransition(transition, firstTime: firstTime) - } - - private func enqueueTransition(_ transition: StickerEntryTransition, firstTime: Bool) { - self.queuedTransitions.append((transition, firstTime)) - if self.validLayout != nil { - self.dequeueTransitions() - } - } - - private func dequeueTransitions() { - while !self.queuedTransitions.isEmpty { - let (transition, firstTime) = self.queuedTransitions.removeFirst() - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { [weak self] _ in - - if let strongSelf = self { - strongSelf.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: strongSelf.bounds.width, height: strongSelf.gridNode.scrollView.contentSize.height + 500.0) - - if firstTime { - let position = strongSelf.gridNode.layer.position - let offset = strongSelf.gridNode.frame.height + strongSelf.gridNode.scrollView.contentOffset.y - strongSelf.gridNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + offset), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in }) - } - } - }) - } - } - - private func topInsetForLayout(size: CGSize) -> CGFloat { - let minimumItemHeights: CGFloat = floor(66.0 * 1.5) - - return max(size.height - minimumItemHeights, 0.0) - } - - override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { - let hadValidLayout = self.validLayout != nil - self.validLayout = (size, leftInset, rightInset, interfaceState) - - var insets = UIEdgeInsets() - insets.top = self.topInsetForLayout(size: size) - insets.left = leftInset - insets.right = rightInset - - transition.updateFrame(node: self.gridNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - - let updateSizeAndInsets = GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: insets, preloadSize: 100.0, type: .fixed(itemSize: CGSize(width: 66.0, height: 66.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition) - - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: updateSizeAndInsets, itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: strongSelf.gridNode.scrollView.contentSize.height + 500.0) - } - }) - - if !hadValidLayout { - self.dequeueTransitions() - } - - if self.theme !== interfaceState.theme { - self.theme = interfaceState.theme - } - } - - override func animateOut(completion: @escaping () -> Void) { - let position = self.gridNode.layer.position - let offset = self.gridNode.frame.height + self.gridNode.scrollView.contentOffset.y - self.gridNode.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - completion() - }) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let convertedPoint = self.convert(point, to: self.gridNode) - if convertedPoint.y > 0.0 { - return super.hitTest(point, with: event) - } else { - return nil - } - } - - private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { - if self.stickersInteraction.previewedStickerItem != item { - self.stickersInteraction.previewedStickerItem = item - - self.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? HorizontalStickerGridItemNode { - itemNode.updatePreviewing(animated: animated) - } - } - } - } -} diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index c6f3b2f5db..80126973e5 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -177,6 +177,7 @@ public final class PresentationCall { private var receptionDisposable: Disposable? private var reportedIncomingCall = false + private var callWasActive = false private var shouldPresentCallRating = false private var sessionStateDisposable: Disposable? @@ -423,14 +424,16 @@ public final class PresentationCall { } } case .accepting: + self.callWasActive = true presentationState = .connecting(nil) case .dropping: presentationState = .terminating case let .terminated(id, reason, options): - presentationState = .terminated(id, reason, options.contains(.reportRating) || self.shouldPresentCallRating) + presentationState = .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)) case let .requesting(ringing): presentationState = .requesting(ringing) case let .active(_, _, keyVisualHash, _, _, _): + self.callWasActive = true if let callContextState = callContextState { switch callContextState { case .initializing: diff --git a/TelegramUI/StickersChatInputContextPanelItem.swift b/TelegramUI/StickersChatInputContextPanelItem.swift new file mode 100644 index 0000000000..1c8d2a8376 --- /dev/null +++ b/TelegramUI/StickersChatInputContextPanelItem.swift @@ -0,0 +1,238 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox + +final class StickersChatInputContextPanelItem: ListViewItem { + let account: Account + let theme: PresentationTheme + let index: Int + let files: [TelegramMediaFile] + let itemsInRow: Int + let stickersInteraction: StickersChatInputContextPanelInteraction + let interfaceInteraction: ChatPanelInterfaceInteraction + + let selectable: Bool = false + + public init(account: Account, theme: PresentationTheme, index: Int, files: [TelegramMediaFile], itemsInRow: Int, stickersInteraction: StickersChatInputContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { + self.account = account + self.theme = theme + self.index = index + self.files = files + self.itemsInRow = itemsInRow + self.stickersInteraction = stickersInteraction + self.interfaceInteraction = interfaceInteraction + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { () -> Void in + let node = StickersChatInputContextPanelItemNode() + + let nodeLayout = node.asyncLayout() + let (top, bottom) = (previousItem != nil, nextItem != nil) + let (layout, apply) = nodeLayout(self, params, top, bottom) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + } + if Thread.isMainThread { + async { + configure() + } + } else { + configure() + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? StickersChatInputContextPanelItemNode { + let nodeLayout = nodeValue.asyncLayout() + + async { + let (top, bottom) = (previousItem != nil, nextItem != nil) + + let (layout, apply) = nodeLayout(self, params, top, bottom) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animation) + }) + } + } + } else { + assertionFailure() + } + } + } +} + +private let itemSize = CGSize(width: 66.0, height: 66.0) +private let inset: CGFloat = 3.0 + +final class StickersChatInputContextPanelItemNode: ListViewItemNode { + private let topSeparatorNode: ASDisplayNode + private var nodes: [TransformImageNode] = [] + private var item: StickersChatInputContextPanelItem? + private let disposables = DisposableSet() + + private var currentPreviewingIndex: Int? + + init() { + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + } + + deinit { + self.disposables.dispose() + } + + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = item as? StickersChatInputContextPanelItem { + let doLayout = self.asyncLayout() + let merged = (top: previousItem != nil, bottom: nextItem != nil) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) + self.contentSize = layout.contentSize + self.insets = layout.insets + apply(.None) + } + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + guard let item = self.item else { + return + } + let location = gestureRecognizer.location(in: gestureRecognizer.view) + for i in 0 ..< self.nodes.count { + if self.nodes[i].frame.contains(location) { + let file = item.files[i] + item.interfaceInteraction.sendSticker(.standalone(media: file)) + break + } + } + } + + func stickerItem(at index: Int) -> StickerPackItem? { + guard let item = self.item else { + return nil + } + if index < item.files.count { + return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.files[index], indexKeys: []) + } else { + return nil + } + } + + func stickerItem(at location: CGPoint) -> (StickerPackItem, ASDisplayNode)? { + guard let item = self.item else { + return nil + } + for i in 0 ..< self.nodes.count { + if self.nodes[i].frame.contains(location) { + return (StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.files[i], indexKeys: []), self.nodes[i]) + } + } + return nil + } + +// func transitionNode() -> ASDisplayNode? { +// return self.imageNode +// } +// + func updatePreviewing(animated: Bool) { + guard let item = self.item else { + return + } + + var previewingIndex: Int? = nil + for i in 0 ..< item.files.count { + if item.stickersInteraction.previewedStickerItem == self.stickerItem(at: i) { + previewingIndex = i + break + } + } + + if self.currentPreviewingIndex != previewingIndex { + self.currentPreviewingIndex = previewingIndex + + for i in 0 ..< self.nodes.count { + let layer = self.nodes[i].layer + if i == previewingIndex { + layer.transform = CATransform3DMakeScale(0.8, 0.8, 1.0) + if animated { + let scale = ((layer.presentation()?.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue ?? (layer.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue) ?? 1.0 + layer.animateSpring(from: scale as NSNumber, to: 0.8 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } else { + layer.transform = CATransform3DIdentity + if animated { + let scale = ((layer.presentation()?.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue ?? (layer.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue) ?? 0.8 + layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + } + } + } + } + + func asyncLayout() -> (_ item: StickersChatInputContextPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, params, mergedTop, mergedBottom in + let baseWidth = params.width - params.leftInset - params.rightInset + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 66.0), insets: UIEdgeInsets()) + + return (nodeLayout, { _ in + if let strongSelf = self { + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.item = item + + if item.index == 0 && strongSelf.topSeparatorNode.supernode == nil { + strongSelf.addSubnode(strongSelf.topSeparatorNode) + } + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) + + let spacing = (baseWidth - itemSize.width * CGFloat(item.itemsInRow)) / (CGFloat(max(1, item.itemsInRow + 1))) + + var i = 0 + for file in item.files { + let imageNode: TransformImageNode + if strongSelf.nodes.count > i { + imageNode = strongSelf.nodes[i] + } else { + imageNode = TransformImageNode() + strongSelf.nodes.append(imageNode) + strongSelf.addSubnode(imageNode) + } + + imageNode.setSignal(chatMessageSticker(account: item.account, file: file, small: true)) + strongSelf.disposables.add(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start()) + + var imageSize = itemSize + if let dimensions = file.dimensions { + imageSize = dimensions.aspectFitted(CGSize(width: itemSize.width - 4.0, height: itemSize.height - 4.0)) + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + } + + imageNode.frame = CGRect(x: spacing + params.leftInset + (itemSize.width + spacing) * CGFloat(i) + floor((itemSize.width - imageSize.width) / 2.0), y: floor((itemSize.height - imageSize.height) / 2.0), width: imageSize.width, height: imageSize.height) + + i += 1 + } + } + }) + } + } +} diff --git a/TelegramUI/StickersChatInputContextPanelNode.swift b/TelegramUI/StickersChatInputContextPanelNode.swift index 0a5af575e7..93989a8bed 100644 --- a/TelegramUI/StickersChatInputContextPanelNode.swift +++ b/TelegramUI/StickersChatInputContextPanelNode.swift @@ -3,69 +3,96 @@ import AsyncDisplayKit import Postbox import TelegramCore import Display +import SwiftSignalKit private struct StickersChatInputContextPanelEntryStableId: Hashable { - let text: String + let ids: [MediaId] var hashValue: Int { - return self.text.hashValue + var hash: Int = 0 + for i in 0 ..< self.ids.count { + if i == 0 { + hash = self.ids[i].hashValue + } else { + hash = hash &* 31 &+ self.ids[i].hashValue + } + } + return hash } static func ==(lhs: StickersChatInputContextPanelEntryStableId, rhs: StickersChatInputContextPanelEntryStableId) -> Bool { - return lhs.text == rhs.text + return lhs.ids == rhs.ids } } -private struct StickersChatInputContextPanelEntry: Comparable, Identifiable { +final class StickersChatInputContextPanelInteraction { + var previewedStickerItem: StickerPackItem? +} + +private struct StickersChatInputContextPanelEntry: Identifiable, Comparable { let index: Int let theme: PresentationTheme - let text: String + let files: [TelegramMediaFile] + let itemsInRow: Int var stableId: StickersChatInputContextPanelEntryStableId { - return StickersChatInputContextPanelEntryStableId(text: self.text) + return StickersChatInputContextPanelEntryStableId(ids: files.compactMap { $0.id }) } - - func withUpdatedTheme(_ theme: PresentationTheme) -> StickersChatInputContextPanelEntry { - return StickersChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text) - } - + static func ==(lhs: StickersChatInputContextPanelEntry, rhs: StickersChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme + return lhs.index == rhs.index && lhs.stableId == rhs.stableId } - + static func <(lhs: StickersChatInputContextPanelEntry, rhs: StickersChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, hashtagSelected: @escaping (String) -> Void) -> ListViewItem { - return StickersChatInputPanelItem(theme: self.theme, text: self.text, hashtagSelected: hashtagSelected) + func withUpdatedTheme(_ theme: PresentationTheme) -> StickersChatInputContextPanelEntry { + return StickersChatInputContextPanelEntry(index: self.index, theme: theme, files: self.files, itemsInRow: itemsInRow) + } + + func item(account: Account, stickersInteraction: StickersChatInputContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> ListViewItem { + return StickersChatInputContextPanelItem(account: account, theme: self.theme, index: self.index, files: self.files, itemsInRow: self.itemsInRow, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction) } } + private struct StickersChatInputContextPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [StickersChatInputContextPanelEntry], to toEntries: [StickersChatInputContextPanelEntry], account: Account, hashtagSelected: @escaping (String) -> Void) -> StickersChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [StickersChatInputContextPanelEntry], to toEntries: [StickersChatInputContextPanelEntry], account: Account, stickersInteraction: StickersChatInputContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> StickersChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction), directionHint: nil) } return StickersChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } +private let itemSize = CGSize(width: 66.0, height: 66.0) + final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { + private let strings: PresentationStrings + private let listView: ListView + private var results: [TelegramMediaFile] = [] private var currentEntries: [StickersChatInputContextPanelEntry]? private var enqueuedTransitions: [(StickersChatInputContextPanelTransition, Bool)] = [] - private var validLayout: (CGSize, CGFloat, CGFloat)? + private var validLayout: (CGSize, CGFloat, CGFloat, ChatPresentationInterfaceState)? + + public var controllerInteraction: ChatControllerInteraction? + private let stickersInteraction: StickersChatInputContextPanelInteraction + + private var stickerPreviewController: StickerPreviewController? override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + self.strings = strings + self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true @@ -73,6 +100,8 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.limitHitTestToNodes = true self.listView.view.disablesInteractiveTransitionGestureRecognizer = true + self.stickersInteraction = StickersChatInputContextPanelInteraction() + super.init(context: context, theme: theme, strings: strings) self.isOpaque = false @@ -81,27 +110,152 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { self.addSubnode(self.listView) } - func updateResults(_ results: [String]) { - var entries: [StickersChatInputContextPanelEntry] = [] - var index = 0 - var stableIds = Set() - for text in results { - let entry = StickersChatInputContextPanelEntry(index: index, theme: self.theme, text: text) - if stableIds.contains(entry.stableId) { - continue + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in + if let strongSelf = self { + let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view) + guard strongSelf.listView.bounds.contains(convertedPoint) else { + return nil + } + + var stickersNode: StickersChatInputContextPanelItemNode? + strongSelf.listView.forEachVisibleItemNode({ itemNode in + if itemNode.frame.contains(convertedPoint), let node = itemNode as? StickersChatInputContextPanelItemNode { + stickersNode = node + } + }) + + if let stickersNode = stickersNode { + let point = strongSelf.listView.view.convert(point, to: stickersNode.view) + if let (item, itemNode) = stickersNode.stickerItem(at: point) { + return strongSelf.context.account.postbox.transaction { transaction -> Bool in + return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) + } + |> deliverOnMainQueue + |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var menuItems: [PeekControllerMenuItem] = [] + menuItems = [ + PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { + controllerInteraction.sendSticker(.standalone(media: item.file), true) + }), + PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() + } + } + }), + PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + loop: for attribute in item.file.attributes { + switch attribute { + case let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: controllerInteraction.navigationController()) + controller.sendSticker = { file in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + controllerInteraction.sendSticker(file, true) + } + } + + controllerInteraction.navigationController()?.view.window?.endEditing(true) + controllerInteraction.presentController(controller, nil) + } + break loop + default: + break + } + } + } + }), + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {}) + ] + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) + } else { + return nil + } + } + } + } + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + return sourceNode + }) + strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) + return controller + } + return nil + }, updateContent: { [weak self] content in + if let strongSelf = self { + var item: StickerPackItem? + if let content = content as? StickerPreviewPeekContent, case let .pack(contentItem) = content.item { + item = contentItem + } + strongSelf.updatePreviewingItem(item: item, animated: true) + } + })) + } + + private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { + if self.stickersInteraction.previewedStickerItem != item { + self.stickersInteraction.previewedStickerItem = item + + self.listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickersChatInputContextPanelItemNode { + itemNode.updatePreviewing(animated: animated) + } } - stableIds.insert(entry.stableId) - entries.append(entry) - index += 1 } - self.prepareTransition(from: self.currentEntries ?? [], to: entries) + } + + func updateResults(_ results: [TelegramMediaFile]) { + self.results = results + + self.commitResults(updateLayout: true) + } + + private func commitResults(updateLayout: Bool = false) { + guard let validLayout = self.validLayout else { + return + } + + var entries: [StickersChatInputContextPanelEntry] = [] + + let itemsInRow = Int(floor((validLayout.0.width - validLayout.1 - validLayout.2) / itemSize.width)) + + var files: [TelegramMediaFile] = [] + var index = entries.count + for i in 0 ..< self.results.count { + files.append(results[i]) + if files.count == itemsInRow { + entries.append(StickersChatInputContextPanelEntry(index: index, theme: self.theme, files: files, itemsInRow: itemsInRow)) + index += 1 + files.removeAll() + } + } + + if !files.isEmpty { + entries.append(StickersChatInputContextPanelEntry(index: index, theme: self.theme, files: files, itemsInRow: itemsInRow)) + } + + if updateLayout { + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate, interfaceState: validLayout.3) + } + + self.prepareTransition(from: self.currentEntries, to: entries) } private func prepareTransition(from: [StickersChatInputContextPanelEntry]? , to: [StickersChatInputContextPanelEntry]) { let firstTime = from == nil - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, hashtagSelected: { [weak self] text in - - }) + let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, stickersInteraction: self.stickersInteraction, interfaceInteraction: self.interfaceInteraction!) self.currentEntries = to self.enqueueTransition(transition, firstTime: firstTime) } @@ -155,14 +309,14 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } private func topInsetForLayout(size: CGSize) -> CGFloat { - let minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5) + let minimumItemHeights: CGFloat = floor(itemSize.height * 1.5) return max(size.height - minimumItemHeights, 0.0) } override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { let hadValidLayout = self.validLayout != nil - self.validLayout = (size, leftInset, rightInset) + self.validLayout = (size, leftInset, rightInset, interfaceState) var insets = UIEdgeInsets() insets.top = self.topInsetForLayout(size: size) @@ -174,16 +328,16 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { var duration: Double = 0.0 var curve: UInt = 0 switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut, .custom: + break + case .spring: + curve = 7 + } } let listViewCurve: ListViewAnimationCurve @@ -197,6 +351,8 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.commitResults(updateLayout: false) + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() @@ -235,4 +391,3 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) } } - diff --git a/TelegramUI/StickersChatInputPanelItem.swift b/TelegramUI/StickersChatInputPanelItem.swift deleted file mode 100644 index 3ea953c990..0000000000 --- a/TelegramUI/StickersChatInputPanelItem.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import TelegramCore -import SwiftSignalKit -import Postbox - -final class StickersChatInputPanelItem: ListViewItem { - fileprivate let theme: PresentationTheme - fileprivate let text: String - private let hashtagSelected: (String) -> Void - - let selectable: Bool = true - - public init(theme: PresentationTheme, text: String, hashtagSelected: @escaping (String) -> Void) { - self.theme = theme - self.text = text - self.hashtagSelected = hashtagSelected - } - - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - let configure = { () -> Void in - let node = StickersChatInputPanelItemNode() - - let nodeLayout = node.asyncLayout() - let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, params, top, bottom) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply(.None) }) - }) - } - } - if Thread.isMainThread { - async { - configure() - } - } else { - configure() - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? StickersChatInputPanelItemNode { - let nodeLayout = nodeValue.asyncLayout() - - async { - let (top, bottom) = (previousItem != nil, nextItem != nil) - - let (layout, apply) = nodeLayout(self, params, top, bottom) - Queue.mainQueue().async { - completion(layout, { _ in - apply(animation) - }) - } - } - } else { - assertionFailure() - } - } - } -} - -private let textFont = Font.medium(14.0) - -final class StickersChatInputPanelItemNode: ListViewItemNode { - static let itemHeight: CGFloat = 42.0 - private let textNode: TextNode - private let topSeparatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - - init() { - self.textNode = TextNode() - - self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.isLayerBacked = true - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.topSeparatorNode) - self.addSubnode(self.textNode) - } - - override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = item as? StickersChatInputPanelItem { - let doLayout = self.asyncLayout() - let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) - self.contentSize = layout.contentSize - self.insets = layout.insets - apply(.None) - } - } - - func asyncLayout() -> (_ item: StickersChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let makeTextLayout = TextNode.asyncLayout(self.textNode) - return { [weak self] item, params, mergedTop, mergedBottom in - let baseWidth = params.width - params.leftInset - params.rightInset - - let leftInset: CGFloat = 15.0 + params.leftInset - let rightInset: CGFloat = 10.0 + params.rightInset - - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "#\(item.text)", font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) - - return (nodeLayout, { _ in - if let strongSelf = self { - strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - - let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) - - strongSelf.topSeparatorNode.isHidden = mergedTop - - - strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) - - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) - } - }) - } - } -} diff --git a/TelegramUI/UrlEscaping.swift b/TelegramUI/UrlEscaping.swift index e4e032c52d..8a02c43ee5 100644 --- a/TelegramUI/UrlEscaping.swift +++ b/TelegramUI/UrlEscaping.swift @@ -32,7 +32,12 @@ extension CharacterSet { } func isValidUrl(_ url: String) -> Bool { - if let url = URL(string: url), ["http", "https"].contains(url.scheme), let host = url.host, host.contains(".") { + if let url = URL(string: url), ["http", "https"].contains(url.scheme), let host = url.host, host.contains(".") && url.user == nil { + let components = host.components(separatedBy: ".") + let domain = (components.first ?? "") + if domain.isEmpty { + return false + } return true } else { return false