diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 9d7b7dbd1f..259b9cfa7e 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */; }; D0104F2A1F471DA6004E4881 /* InstantImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */; }; D0104F2C1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */; }; + D0105D682182680E007C04A7 /* IsMediaStreamable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D672182680E007C04A7 /* IsMediaStreamable.swift */; }; D0119CD020CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0119CCF20CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift */; }; D013630C208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D013630B208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift */; }; D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0147BA6206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift */; }; @@ -251,6 +252,9 @@ D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */; }; D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */; }; D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */; }; + D081E104217F57D2003CD921 /* LanguageLinkPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081E103217F57D2003CD921 /* LanguageLinkPreviewController.swift */; }; + D081E106217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081E105217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift */; }; + D081E108217F583F003CD921 /* LanguageLinkPreviewContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081E107217F583F003CD921 /* LanguageLinkPreviewContentNode.swift */; }; D083491C209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */; }; D084023420E295F000065674 /* GroupStickerPackSetupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084023320E295F000065674 /* GroupStickerPackSetupController.swift */; }; D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */; }; @@ -1135,6 +1139,7 @@ D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantImageGalleryItem.swift; sourceTree = ""; }; D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageGalleryFooterContentNode.swift; sourceTree = ""; }; D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; + D0105D672182680E007C04A7 /* IsMediaStreamable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsMediaStreamable.swift; sourceTree = ""; }; D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeSettings.swift; sourceTree = ""; }; D010C2CB1EA7D74800F41B96 /* DefaultPresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPresentationTheme.swift; sourceTree = ""; }; D010C2CD1EA7DDD600F41B96 /* DefaultPresentationStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPresentationStrings.swift; sourceTree = ""; }; @@ -1555,6 +1560,9 @@ D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTitleProxyNode.swift; sourceTree = ""; }; D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyServerActionSheetController.swift; sourceTree = ""; }; D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageManagedMediaId.swift; sourceTree = ""; }; + D081E103217F57D2003CD921 /* LanguageLinkPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageLinkPreviewController.swift; sourceTree = ""; }; + D081E105217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageLinkPreviewControllerNode.swift; sourceTree = ""; }; + D081E107217F583F003CD921 /* LanguageLinkPreviewContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageLinkPreviewContentNode.swift; sourceTree = ""; }; D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarGalleryItemFooterContentNode.swift; sourceTree = ""; }; D084023320E295F000065674 /* GroupStickerPackSetupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStickerPackSetupController.swift; sourceTree = ""; }; D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListEditableDeleteControlNode.swift; sourceTree = ""; }; @@ -3047,6 +3055,16 @@ name = "Chat List Node"; sourceTree = ""; }; + D081E102217F57B2003CD921 /* Language Link Preview */ = { + isa = PBXGroup; + children = ( + D081E103217F57D2003CD921 /* LanguageLinkPreviewController.swift */, + D081E105217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift */, + D081E107217F583F003CD921 /* LanguageLinkPreviewContentNode.swift */, + ); + name = "Language Link Preview"; + sourceTree = ""; + }; D087750A1E3E7A6D00A97350 /* Settings */ = { isa = PBXGroup; children = ( @@ -3910,6 +3928,7 @@ D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */, D0FA08BF20483F9600DD23FC /* ExtractVideoData.swift */, D0ADF965212E05A300310BBC /* TonePlayer.swift */, + D0105D672182680E007C04A7 /* IsMediaStreamable.swift */, ); name = Media; sourceTree = ""; @@ -4226,6 +4245,7 @@ D0D748041E7AF62000F4B1F6 /* Stickers */, D020A9D81FEAE611008C66F7 /* Player */, D01C06AD1FBB45ED001561AB /* Join Link Preview */, + D081E102217F57B2003CD921 /* Language Link Preview */, ); name = Media; sourceTree = ""; @@ -4823,6 +4843,7 @@ D01C06C01FBF118A001561AB /* MessageUtils.swift in Sources */, D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */, D0EC6CC81EB9F58800EBF1C3 /* ProgressiveImage.swift in Sources */, + D081E108217F583F003CD921 /* LanguageLinkPreviewContentNode.swift in Sources */, D0EC6CC91EB9F58800EBF1C3 /* WebP.swift in Sources */, D0EC6CCA1EB9F58800EBF1C3 /* PeerPresenceStatusManager.swift in Sources */, D0EC6CCB1EB9F58800EBF1C3 /* ApplicationSpecificData.swift in Sources */, @@ -5005,6 +5026,7 @@ D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */, D04281EF200E3D88009DDE36 /* GroupInfoSearchItem.swift in Sources */, D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */, + D081E104217F57D2003CD921 /* LanguageLinkPreviewController.swift in Sources */, D0EC6FFD1EBA1F2400EBF1C3 /* OngoingCallThreadLocalContext.mm in Sources */, D0E9BAE21F0574D800F079A4 /* STPBankAccount.m in Sources */, D0104F2A1F471DA6004E4881 /* InstantImageGalleryItem.swift in Sources */, @@ -5041,6 +5063,7 @@ D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */, D0EC6D391EB9F58800EBF1C3 /* ImageContainingNode.swift in Sources */, D0EC6D3A1EB9F58800EBF1C3 /* AudioWaveformNode.swift in Sources */, + D0105D682182680E007C04A7 /* IsMediaStreamable.swift in Sources */, D0EB41F71F30D4A800838FE6 /* LegacyMediaLocations.swift in Sources */, D0EC6D3B1EB9F58800EBF1C3 /* EditableTokenListNode.swift in Sources */, D0EC6D3C1EB9F58800EBF1C3 /* PhoneInputNode.swift in Sources */, @@ -5548,6 +5571,7 @@ D0EC6E531EB9F58900EBF1C3 /* ChannelMembersController.swift in Sources */, D02B676320800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift in Sources */, D093D8242069A06600BC3599 /* FormControllerScrollerNode.swift in Sources */, + D081E106217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift in Sources */, D093D7E72063E57F00BC3599 /* BotPaymentActionItemNode.swift in Sources */, D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */, D0EC6E541EB9F58900EBF1C3 /* ConvertToSupergroupController.swift in Sources */, diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index d69987481c..95c1f132d3 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -311,7 +311,7 @@ public final class AvatarNode: ASDisplayNode { if peerId.namespace == -1 { colorIndex = -1 } else { - colorIndex = abs(Int(accountPeerId.id + peerId.id)) + colorIndex = abs(Int(clamping: accountPeerId.id &+ peerId.id)) } } else { colorIndex = -1 diff --git a/TelegramUI/CallListControllerNode.swift b/TelegramUI/CallListControllerNode.swift index 7b3069af48..597fb9db8a 100644 --- a/TelegramUI/CallListControllerNode.swift +++ b/TelegramUI/CallListControllerNode.swift @@ -505,7 +505,7 @@ final class CallListControllerNode: ASDisplayNode { func scrollToLatest() { if let view = self.callListView?.originalView, view.later == nil { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .top(0.0), animated: true) self.currentLocationAndType = CallListNodeLocationAndType(location: location, type: self.currentLocationAndType.type) @@ -562,7 +562,7 @@ final class CallListControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/CallListViewTransition.swift b/TelegramUI/CallListViewTransition.swift index fdf0d6cdce..b982dc2d76 100644 --- a/TelegramUI/CallListViewTransition.swift +++ b/TelegramUI/CallListViewTransition.swift @@ -150,7 +150,7 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index -= 1 @@ -160,7 +160,7 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV var index = 0 for entry in toView.filteredEntries.reversed() { if entry.index < scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index += 1 diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index 6af3a6ac1a..742228c46e 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -464,7 +464,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: nil) } self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 0a443a797c..692a1d5607 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -253,7 +253,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) @@ -358,6 +358,6 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index d7f60e491a..d38de12151 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -278,7 +278,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID return true } - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, mode in guard let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else { return false } @@ -298,6 +298,13 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID } case let .photoUpdated(image): openMessageByAction = image != nil + case .gameScore: + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) + break + } + } default: break } @@ -306,6 +313,12 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID } } } + + if case .stream = mode { + strongSelf.debugStreamSingleVideo(message.id) + return true + } + return openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a in @@ -395,9 +408,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID break } } - let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, interfaceInteraction: strongSelf.interfaceInteraction, debugStreamSingleVideo: { id in - self?.debugStreamSingleVideo(id) - }).start(next: { actions in + let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, interfaceInteraction: strongSelf.interfaceInteraction).start(next: { actions in guard let strongSelf = self, !actions.isEmpty else { return } @@ -1710,7 +1721,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID if let strongSelf = self, let validLayout = strongSelf.validLayout { var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? - strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets, _, _ in + strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut), listViewTransaction: { updateSizeAndInsets, _, _ in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -1731,7 +1742,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) } - let scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up) + let scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: 0.2), directionHint: .Up) var stationaryItemRange: (Int, Int)? if let maxInsertedItem = maxInsertedItem { @@ -2824,7 +2835,9 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID return } if let currentItem = currentItem?.id as? PeerMessagesMediaPlaylistItemId, let previousItem = previousItem?.id as? PeerMessagesMediaPlaylistItemId, previousItem.messageId.peerId == peerId, currentItem.messageId.peerId == peerId, currentItem.messageId != previousItem.messageId { - strongSelf.navigateToMessage(from: nil, to: .id(currentItem.messageId), scrollPosition: .center(.bottom), rememberInStack: false, animated: true, completion: nil) + if strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(currentItem.messageId) != nil { + strongSelf.navigateToMessage(from: nil, to: .id(currentItem.messageId), scrollPosition: .center(.bottom), rememberInStack: false, animated: true, completion: nil) + } } } } @@ -3797,7 +3810,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID private func activateRaiseGesture() { if let messageToListen = self.firstLoadedMessageToListen() { - let _ = self.controllerInteraction?.openMessage(messageToListen) + let _ = self.controllerInteraction?.openMessage(messageToListen, .default) } else { self.requestAudioRecorder(beginWithTone: true) } @@ -4587,6 +4600,8 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID default: break } + }, sendFile: { [weak self] f in + self?.interfaceInteraction?.sendSticker(f) }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, dismissInput: { [weak self] in diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 5299618d5e..24d65859d4 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -37,8 +37,13 @@ public enum ChatControllerInteractionLongTapAction { case hashtag(String) } +public enum ChatControllerInteractionOpenMessageMode { + case `default` + case stream +} + public final class ChatControllerInteraction { - let openMessage: (Message) -> Bool + let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, ASDisplayNode, CGRect) -> Void @@ -79,7 +84,7 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32)->Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32)->Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 593e00c804..840b29ccfb 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -707,7 +707,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } var accessoryPanelSize: CGSize? diff --git a/TelegramUI/ChatHistorySearchContainerNode.swift b/TelegramUI/ChatHistorySearchContainerNode.swift index e5f83bfacf..d3fc25ff25 100644 --- a/TelegramUI/ChatHistorySearchContainerNode.swift +++ b/TelegramUI/ChatHistorySearchContainerNode.swift @@ -223,7 +223,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if firstValidLayout { diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 2c2b444dbf..cdc3e5f6e9 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -149,7 +149,7 @@ func updatedChatEditInterfaceMessagetState(state: ChatPresentationInterfaceState return updated } -func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, messages: [Message], controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, debugStreamSingleVideo: @escaping (MessageId) -> Void) -> Signal<[ChatMessageContextMenuAction], NoError> { +func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, messages: [Message], controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal<[ChatMessageContextMenuAction], NoError> { guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single([]) } @@ -418,10 +418,6 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_LinkDialogSave, action: { let _ = addSavedGif(postbox: account.postbox, fileReference: .message(message: MessageReference(message), media: file)).start() }))) - } else if !GlobalExperimentalSettings.isAppStoreBuild { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: "Stream", action: { - debugStreamSingleVideo(message.id) - }))) } break } @@ -442,7 +438,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if data.messageActions.options.contains(.viewStickerPack) { actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.StickerPack_ViewPack, action: { - let _ = controllerInteraction.openMessage(message) + let _ = controllerInteraction.openMessage(message, .default) }))) } diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index 20bec02b39..b394b3522e 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -102,7 +102,7 @@ class ChatListControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 198a3779dc..20732cf167 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -353,6 +353,8 @@ final class ChatListNode: ListView { super.init() + //self.verticalScrollIndicatorColor = UIColor(white: 0.3, alpha: 0.8) + let nodeInteraction = ChatListNodeInteraction(activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() @@ -941,7 +943,7 @@ final class ChatListNode: ListView { } if view.laterIndex == nil { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound , scrollPosition: .top(0.0), animated: true) diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index d1cd500e65..a274f40fa2 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -877,7 +877,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index 5823bbf795..bcd6e25bcf 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -264,7 +264,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: size.width) self.listView.position = CGPoint(x: size.width / 2.0, y: 92.0 / 2.0 + 29.0) - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: size.width), insets: insets, duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: size.width), insets: insets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) itemCustomWidthValuePromise.set(itemCustomWidth) } diff --git a/TelegramUI/ChatListViewTransition.swift b/TelegramUI/ChatListViewTransition.swift index 67b13e5343..5ca0474eba 100644 --- a/TelegramUI/ChatListViewTransition.swift +++ b/TelegramUI/ChatListViewTransition.swift @@ -164,7 +164,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index -= 1 @@ -174,7 +174,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV var index = 0 for entry in toView.filteredEntries.reversed() { if entry.index < scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index += 1 diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index bd591bdcc3..72737b3846 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -984,7 +984,7 @@ final class ChatMediaInputNode: ChatInputNode { let firstVisibleIndex = currentView.collectionInfos.index(where: { id, _, _ in return id == firstVisibleCollectionId }) if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { let toRight = targetIndex > firstVisibleIndex - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default, directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) } } } @@ -1126,7 +1126,7 @@ final class ChatMediaInputNode: ChatInputNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: listViewCurve) diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index 377348edb5..18aadbf3c5 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -199,7 +199,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) var duration: Double = 0.0 - var listViewCurve: ListViewAnimationCurve = .Default + var listViewCurve: ListViewAnimationCurve = .Default(duration: nil) switch transition { case .immediate: break @@ -207,7 +207,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { duration = animationDuration switch animationCurve { case .easeInOut: - listViewCurve = .Default + listViewCurve = .Default(duration: duration) case .spring: listViewCurve = .Spring(duration: duration) } diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index 37c53449fb..6b01901fd1 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -225,7 +225,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { private var media: Media? private var theme: ChatPresentationThemeData? - var openMedia: (() -> Void)? + var openMedia: ((Bool) -> Void)? var activateAction: (() -> Void)? var visibility: ListViewItemNodeVisibility = .none { @@ -739,9 +739,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if strongSelf.contentImageNode !== contentImageNode { strongSelf.contentImageNode = contentImageNode strongSelf.addSubnode(contentImageNode) - contentImageNode.activateLocalContent = { [weak strongSelf] in + contentImageNode.activateLocalContent = { [weak strongSelf] mode in if let strongSelf = strongSelf { - strongSelf.openMedia?() + strongSelf.openMedia?(mode == .stream) } } contentImageNode.visibility = strongSelf.visibility @@ -772,7 +772,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge contentImageNode.addSubnode(updatedAdditionalImageBadge) updatedAdditionalImageBadge.contentMode = .topRight - updatedAdditionalImageBadge.content = additionalImageBadgeContent + updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false) updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 2.0, y: contentImageSize.height - 18.0 - 2.0), size: CGSize(width: 0.0, height: 0.0)) } else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode { strongSelf.additionalImageBadgeNode = nil @@ -788,7 +788,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.addSubnode(contentFileNode) contentFileNode.activateLocalContent = { [weak strongSelf] in if let strongSelf = strongSelf { - strongSelf.openMedia?() + strongSelf.openMedia?(false) } } } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 27569668a6..fe21928b75 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -1574,7 +1574,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case .openMessage: foundTapAction = true if let item = self.item { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) } break loop } diff --git a/TelegramUI/ChatMessageContactBubbleContentNode.swift b/TelegramUI/ChatMessageContactBubbleContentNode.swift index ad7585b2f5..5e1a40cbb8 100644 --- a/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -319,14 +319,14 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { @objc func contactTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) } } } @objc private func buttonPressed() { if let item = self.item { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) } } } diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 2959603b5c..6793126d7e 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -18,7 +18,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveFileNode.activateLocalContent = { [weak self] in if let strongSelf = self { if let item = strongSelf.item { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) } } } diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift index da39bb1a50..649ad762a2 100644 --- a/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -22,7 +22,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { super.init() self.addSubnode(self.contentNode) - self.contentNode.openMedia = { [weak self] in + self.contentNode.openMedia = { [weak self] _ in if let strongSelf = self, let item = strongSelf.item { item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true) } diff --git a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift index d8f3ca9f4b..9b909a724c 100644 --- a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift +++ b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift @@ -581,7 +581,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if self.infoBackgroundNode.alpha.isZero { item.account.telegramApplicationContext.mediaManager?.playlistControl(.playback(.togglePlayPause), type: .voice) } else { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) } } diff --git a/TelegramUI/ChatMessageInteractiveMediaBadge.swift b/TelegramUI/ChatMessageInteractiveMediaBadge.swift index 0d1171b48c..6d9d1d805d 100644 --- a/TelegramUI/ChatMessageInteractiveMediaBadge.swift +++ b/TelegramUI/ChatMessageInteractiveMediaBadge.swift @@ -5,27 +5,16 @@ import AsyncDisplayKit enum ChatMessageInteractiveMediaBadgeShape: Equatable { case round case corners(CGFloat) - - static func ==(lhs: ChatMessageInteractiveMediaBadgeShape, rhs: ChatMessageInteractiveMediaBadgeShape) -> Bool { - switch lhs { - case .round: - if case .round = rhs { - return true - } else { - return false - } - case let .corners(radius): - if case .corners(radius) = rhs { - return true - } else { - return false - } - } - } +} + +enum ChatMessageInteractiveMediaDownloadState: Equatable { + case remote + case fetching(progress: Float) } enum ChatMessageInteractiveMediaBadgeContent: Equatable { case text(backgroundColor: UIColor, foregroundColor: UIColor, shape: ChatMessageInteractiveMediaBadgeShape, text: NSAttributedString) + case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String) static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool { switch lhs { @@ -35,6 +24,12 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable { } else { return false } + case let .mediaDownload(lhsBackgroundColor, lhsForegroundColor, lhsDuration, lhsSize): + if case let .mediaDownload(rhsBackgroundColor, rhsForegroundColor, rhsDuration, rhsSize) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsDuration == rhsDuration, lhsSize == rhsSize { + return true + } else { + return false + } } } } @@ -51,22 +46,78 @@ private final class ChatMessageInteractiveMediaBadgeParams: NSObject { } final class ChatMessageInteractiveMediaBadge: ASDisplayNode { - var content: ChatMessageInteractiveMediaBadgeContent? { - didSet { - if oldValue != self.content { - self.setNeedsDisplay() - } - } - } + var pressed: (() -> Void)? + + private var content: ChatMessageInteractiveMediaBadgeContent? + + private var mediaDownloadStatusNode: RadialStatusNode? + private var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? override init() { super.init() - self.isLayerBacked = true self.contentMode = .topLeft self.contentsScale = UIScreenScale } + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.pressed?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let contents = self.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { + let image = contents as! CGImage + if CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(image.width) / UIScreenScale, height: CGFloat(image.height) / UIScreenScale)).contains(point) { + return self.view + } + } + return nil + } + + func update(theme: PresentationTheme, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, animated: Bool) { + if self.content != content { + self.content = content + self.setNeedsDisplay() + } + if self.mediaDownloadState != mediaDownloadState { + self.mediaDownloadState = mediaDownloadState + if let mediaDownloadState = self.mediaDownloadState { + let mediaDownloadStatusNode: RadialStatusNode + if let current = self.mediaDownloadStatusNode { + mediaDownloadStatusNode = current + } else { + mediaDownloadStatusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.mediaDownloadStatusNode = mediaDownloadStatusNode + mediaDownloadStatusNode.frame = CGRect(origin: CGPoint(x: 7.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)) + self.addSubnode(mediaDownloadStatusNode) + } + let state: RadialStatusNodeState + switch mediaDownloadState { + case .remote: + if let image = PresentationResourcesChat.chatBubbleFileCloudFetchMediaIcon(theme) { + state = .customIcon(image) + } else { + state = .none + } + case let .fetching(progress): + state = .cloudProgress(color: .white, strokeBackgroundColor: UIColor(white: 1.0, alpha: 0.3), lineWidth: 2.0, value: CGFloat(progress)) + } + mediaDownloadStatusNode.transitionToState(state, animated: true, completion: {}) + } else if let mediaDownloadStatusNode = self.mediaDownloadStatusNode { + self.mediaDownloadStatusNode = nil + mediaDownloadStatusNode.removeFromSupernode() + } + } + } + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return ChatMessageInteractiveMediaBadgeParams(content: self.content) } @@ -107,6 +158,34 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { convertedText.draw(at: CGPoint(x: floor((size.width - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y)) UIGraphicsPopContext() }) + case let .mediaDownload(backgroundColor, foregroundColor, duration, size): + let durationString = NSMutableAttributedString(string: duration, attributes: [.font: font, .foregroundColor: foregroundColor]) + let sizeString = NSMutableAttributedString(string: size, attributes: [.font: font, .foregroundColor: foregroundColor]) + let durationRect = durationString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let sizeRect = sizeString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let leftInset: CGFloat = 42.0 + let imageSize = CGSize(width: leftInset + max(ceil(durationRect.width), ceil(sizeRect.width)) + 10.0, height: 40.0) + return generateImage(imageSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + context.setBlendMode(.copy) + context.setFillColor(backgroundColor.cgColor) + + let radius: CGFloat = 12.0 + let diameter = radius * 2.0 + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - diameter), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: 0.0), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: size.height - diameter), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: diameter, height: size.height - diameter))) + context.fill(CGRect(origin: CGPoint(x: radius, y: 0.0), size: CGSize(width: size.width - diameter, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: size.width - diameter, y: radius), size: CGSize(width: diameter, height: size.height - diameter))) + + context.setBlendMode(.normal) + UIGraphicsPushContext(context) + durationString.draw(at: CGPoint(x: leftInset + durationRect.origin.x, y: 7.0 + durationRect.origin.y)) + sizeString.draw(at: CGPoint(x: leftInset + sizeRect.origin.x, y: 21.0 + sizeRect.origin.y)) + UIGraphicsPopContext() + }) } } return nil diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 43f27b17ed..b11dfe926a 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -15,6 +15,11 @@ enum InteractiveMediaNodeSizeCalculation { case unconstrained } +enum InteractiveMediaNodeActivateContent { + case `default` + case stream +} + final class ChatMessageInteractiveMediaNode: ASDisplayNode { private let imageNode: TransformImageNode private var videoNode: UniversalVideoNode? @@ -54,7 +59,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } } - var activateLocalContent: () -> Void = { } + var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } override init() { self.imageNode = TransformImageNode() @@ -80,8 +85,18 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { self.tapRecognizer = tapRecognizer } - @objc func progressPressed() { + private func progressPressed(canActivate: Bool) { if let fetchStatus = self.fetchStatus { + if canActivate, let state = self.statusNode?.state, case .play = state { + switch fetchStatus { + case .Remote, .Fetching: + self.activateLocalContent(.stream) + default: + break + } + return + } + switch fetchStatus { case .Fetching: if let account = self.account, let message = self.message, message.flags.isSending { @@ -112,14 +127,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if case .ended = recognizer.state { let point = recognizer.location(in: self.imageNode.view) if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { - self.activateLocalContent() + self.activateLocalContent(.default) } else { if let message = self.message, message.flags.isSending { if let statusNode = self.statusNode, statusNode.frame.contains(point) { - self.progressPressed() + self.progressPressed(canActivate: true) } } else { - self.progressPressed() + self.progressPressed(canActivate: true) } } } @@ -537,6 +552,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { var state: RadialStatusNodeState = .none var badgeContent: ChatMessageInteractiveMediaBadgeContent? + var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? let bubbleTheme = theme.chat.bubble if let invoice = invoice { let string = NSMutableAttributedString() @@ -573,10 +589,23 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } else { state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) } + if case .constrained = sizeCalculation { if let file = media as? TelegramMediaFile, (!file.isAnimated || message.flags.contains(.Unsent)) { if let size = file.size { - badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: "\(dataSizeString(Int(Float(size) * progress))) / \(dataSizeString(size))")) + if let duration = file.duration { + if isMediaStreamable(message: message, media: file) { + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))" + badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: sizeString) + mediaDownloadState = .fetching(progress: progress) + state = .play(bubbleTheme.mediaOverlayControlForegroundColor) + } else { + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))")) + } + } else { + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))")) + } } else if let _ = file.duration { badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: strings.Conversation_Processing)) } @@ -614,9 +643,18 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { case .Remote: state = .download(bubbleTheme.mediaOverlayControlForegroundColor) if case .constrained = sizeCalculation { - if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) - badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) + if let file = self.media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { + if isMediaStreamable(message: message, media: file) { + state = .play(bubbleTheme.mediaOverlayControlForegroundColor) + + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + + badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0)) + mediaDownloadState = .remote + } else { + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) + } } } } @@ -648,10 +686,21 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if self.badgeNode == nil { let badgeNode = ChatMessageInteractiveMediaBadge() badgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + badgeNode.pressed = { [weak self] in + guard let strongSelf = self, let fetchStatus = strongSelf.fetchStatus else { + return + } + switch fetchStatus { + case .Remote, .Fetching: + strongSelf.progressPressed(canActivate: false) + default: + break + } + } self.badgeNode = badgeNode self.addSubnode(badgeNode) } - self.badgeNode?.content = badgeContent + self.badgeNode?.update(theme: theme, content: badgeContent, mediaDownloadState: mediaDownloadState, animated: false) } else if let badgeNode = self.badgeNode { self.badgeNode = nil badgeNode.removeFromSupernode() diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index a31aed59de..dbf751217b 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -132,7 +132,7 @@ public class ChatMessageItemView: ListViewItemNode { override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { if short { - self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } else { self.transitionOffset = -self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift index 235a42bf80..d61b51fa33 100644 --- a/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -452,7 +452,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) } } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 6a1e1e00da..23fc6f62b7 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -31,10 +31,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.interactiveImageNode) - self.interactiveImageNode.activateLocalContent = { [weak self] in + self.interactiveImageNode.activateLocalContent = { [weak self] mode in if let strongSelf = self { if let item = strongSelf.item { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, mode == .stream ? .stream : .default) } } } @@ -226,7 +226,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { - if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(to: media) { + if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isSemanticallyEqual(to: media) { let interactiveImageNode = self.interactiveImageNode return (self.interactiveImageNode, { [weak interactiveImageNode] in return interactiveImageNode?.view.snapshotContentTree(unhide: true) diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 0e6319e47e..991ae0786a 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -354,7 +354,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if let item = self.item, self.imageNode.frame.contains(location) { - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, .default) return } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 7cab20bd84..228551ef91 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -120,7 +120,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { super.init() self.addSubnode(self.contentNode) - self.contentNode.openMedia = { [weak self] in + self.contentNode.openMedia = { [weak self] stream in if let strongSelf = self, let item = strongSelf.item { if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content, let image = content.image, let instantPage = content.instantPage { var isGallery = false @@ -138,7 +138,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return } } - let _ = item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message, stream ? .stream : .default) } } self.contentNode.activateAction = { [weak self] in diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 138265f7bf..957e9e4468 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -120,7 +120,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }) self.adminsDisposable = adminsDisposable - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, _ in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { guard let state = strongSelf.listNode.opaqueTransactionState as? ChatRecentActionsListOpaqueState else { return false @@ -479,7 +479,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let contentBottomInset: CGFloat = panelHeight + 4.0 @@ -730,6 +730,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.openPeer(peerId: peerId, peer: nil) } }), nil) + case let .localization(identifier): + strongSelf.presentController(LanguageLinkPreviewController(account: strongSelf.account, identifier: identifier), nil) case .proxy: openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, _ in if let strongSelf = self { diff --git a/TelegramUI/CommandChatInputContextPanelNode.swift b/TelegramUI/CommandChatInputContextPanelNode.swift index 226c0cd3f1..c2f797b3ad 100644 --- a/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/TelegramUI/CommandChatInputContextPanelNode.swift @@ -164,7 +164,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { insets.left = validLayout.1 insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default(duration: nil)) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -220,7 +220,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index 6929f7339c..00c7c06c95 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -824,7 +824,7 @@ final class ContactListNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) @@ -872,6 +872,6 @@ final class ContactListNode: ASDisplayNode { } func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 8431e1b512..b26a756568 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -320,7 +320,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { diff --git a/TelegramUI/CreatePasswordController.swift b/TelegramUI/CreatePasswordController.swift index 5166df796e..94bb7ed50f 100644 --- a/TelegramUI/CreatePasswordController.swift +++ b/TelegramUI/CreatePasswordController.swift @@ -270,18 +270,18 @@ func createPasswordController(account: Account, context: CreatePasswordContext, switch update { case .none: break - case let .password(password, pendingEmailPattern): - if let pendingEmailPattern = pendingEmailPattern { + case let .password(password, pendingEmail): + if let pendingEmail = pendingEmail { if processPasswordEmailConfirmation { updateState { state in var state = state state.saving = false - state.state = .pendingVerification(emailPattern: pendingEmailPattern) + state.state = .pendingVerification(emailPattern: pendingEmail.pattern) return state } } - updatePasswordEmailConfirmation(pendingEmailPattern) + updatePasswordEmailConfirmation(pendingEmail.pattern) } else { completion(password, state.hintText, !state.emailText.isEmpty) } diff --git a/TelegramUI/DocumentPreviewController.swift b/TelegramUI/DocumentPreviewController.swift index 32ed19e19f..d5f1f6f9b7 100644 --- a/TelegramUI/DocumentPreviewController.swift +++ b/TelegramUI/DocumentPreviewController.swift @@ -30,6 +30,8 @@ final class DocumentPreviewController: UINavigationController, QLPreviewControll private var item: DocumentPreviewItem? + private var tempFile: TempBoxFile? + init(theme: PresentationTheme, strings: PresentationStrings, postbox: Postbox, file: TelegramMediaFile) { self.postbox = postbox self.file = file @@ -55,16 +57,14 @@ final class DocumentPreviewController: UINavigationController, QLPreviewControll controller.navigationItem.setLeftBarButton(UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) self.setViewControllers([controller], animated: false) - var pathExtension: String? - if let fileName = self.file.fileName { - let pathExtensionCandidate = (fileName as NSString).pathExtension - if !pathExtensionCandidate.isEmpty { - pathExtension = pathExtensionCandidate + if let path = self.postbox.mediaBox.completedResourcePath(self.file.resource) { + var updatedPath = path + if let fileName = self.file.fileName { + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + updatedPath = tempFile.path + self.tempFile = tempFile } - } - - if let path = self.postbox.mediaBox.completedResourcePath(self.file.resource, pathExtension: pathExtension) { - self.item = DocumentPreviewItem(url: URL(fileURLWithPath: path), title: self.file.fileName ?? strings.Message_File) + self.item = DocumentPreviewItem(url: URL(fileURLWithPath: updatedPath), title: self.file.fileName ?? strings.Message_File) } } @@ -72,6 +72,12 @@ final class DocumentPreviewController: UINavigationController, QLPreviewControll fatalError("init(coder:) has not been implemented") } + deinit { + if let tempFile = self.tempFile { + TempBox.shared.dispose(tempFile) + } + } + @objc private func cancelPressed() { self.presentingViewController?.dismiss(animated: true, completion: nil) } @@ -108,6 +114,8 @@ final class CompactDocumentPreviewController: QLPreviewController, QLPreviewCont private var item: DocumentPreviewItem? + private var tempFile: TempBoxFile? + init(theme: PresentationTheme, strings: PresentationStrings, postbox: Postbox, file: TelegramMediaFile) { self.postbox = postbox self.file = file @@ -137,8 +145,14 @@ final class CompactDocumentPreviewController: QLPreviewController, QLPreviewCont } } - if let path = self.postbox.mediaBox.completedResourcePath(self.file.resource, pathExtension: pathExtension) { - self.item = DocumentPreviewItem(url: URL(fileURLWithPath: path), title: self.file.fileName ?? strings.Message_File) + if let path = self.postbox.mediaBox.completedResourcePath(self.file.resource) { + var updatedPath = path + if let fileName = self.file.fileName { + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + updatedPath = tempFile.path + self.tempFile = tempFile + } + self.item = DocumentPreviewItem(url: URL(fileURLWithPath: updatedPath), title: self.file.fileName ?? strings.Message_File) } } @@ -146,6 +160,12 @@ final class CompactDocumentPreviewController: QLPreviewController, QLPreviewCont fatalError("init(coder:) has not been implemented") } + deinit { + if let tempFile = self.tempFile { + TempBox.shared.dispose(tempFile) + } + } + @objc private func cancelPressed() { self.presentingViewController?.dismiss(animated: true, completion: nil) } @@ -177,9 +197,9 @@ final class CompactDocumentPreviewController: QLPreviewController, QLPreviewCont } func presentDocumentPreviewController(rootController: UIViewController, theme: PresentationTheme, strings: PresentationStrings, postbox: Postbox, file: TelegramMediaFile) { - if #available(iOS 10.0, *) { + /*if #available(iOS 10.0, *) { rootController.present(DocumentPreviewController(theme: theme, strings: strings, postbox: postbox, file: file), animated: true, completion: nil) - } else { + } else {*/ if #available(iOSApplicationExtension 9.0, *) { let navigationBar = UINavigationBar.appearance(whenContainedInInstancesOf: [QLPreviewController.self]) navigationBar.barTintColor = theme.rootController.navigationBar.backgroundColor @@ -193,5 +213,5 @@ func presentDocumentPreviewController(rootController: UIViewController, theme: P } rootController.present(CompactDocumentPreviewController(theme: theme, strings: strings, postbox: postbox, file: file), animated: true, completion: nil) - } + //} } diff --git a/TelegramUI/EmojisChatInputContextPanelNode.swift b/TelegramUI/EmojisChatInputContextPanelNode.swift index 887efc1edd..cff19074b7 100644 --- a/TelegramUI/EmojisChatInputContextPanelNode.swift +++ b/TelegramUI/EmojisChatInputContextPanelNode.swift @@ -157,7 +157,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { insets.left = validLayout.1 insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default(duration: nil)) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -213,7 +213,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index 989b63e502..2441c5266b 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -78,7 +78,8 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa let data: Signal let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1) let readCount = min(resourceSize - context.readingOffset, Int(bufferSize)) - data = postbox.mediaBox.resourceData(resourceReference.resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) + let requestRange: Range = context.readingOffset ..< (context.readingOffset + readCount) + data = postbox.mediaBox.resourceData(resourceReference.resource, size: resourceSize, in: requestRange, mode: .complete) let semaphore = DispatchSemaphore(value: 0) if readCount == 0 { fetchedData = Data() @@ -169,10 +170,10 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe if context.readingOffset >= resourceSize { context.fetchedDataDisposable.set(nil) - context.requestedCompleteFetch = false } else { if streamable { - context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< Int(Int32.max), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + let fetchRange: Range = context.readingOffset ..< Int(Int32.max) + context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) } else if !context.requestedCompleteFetch && context.fetchAutomatically { context.requestedCompleteFetch = true context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) @@ -199,6 +200,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { fileprivate var requestedDataOffset: Int? fileprivate let fetchedDataDisposable = MetaDisposable() + fileprivate let fetchedFullDataDisposable = MetaDisposable() fileprivate var requestedCompleteFetch = false fileprivate var readingError = false @@ -216,7 +218,8 @@ final class FFMpegMediaFrameSourceContext: NSObject { deinit { assert(Thread.current === self.thread) - fetchedDataDisposable.dispose() + self.fetchedDataDisposable.dispose() + self.fetchedFullDataDisposable.dispose() } func initializeState(postbox: Postbox, resourceReference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool) { @@ -233,13 +236,11 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.preferSoftwareDecoding = preferSoftwareDecoding self.fetchAutomatically = fetchAutomatically - let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1) - if streamable { - self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< Int(Int32.max), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: (0 ..< Int(Int32.max), .elevated), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } else if !self.requestedCompleteFetch && self.fetchAutomatically { self.requestedCompleteFetch = true - self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.fetchedFullDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } var avFormatContextRef = avformat_alloc_context() @@ -395,6 +396,11 @@ final class FFMpegMediaFrameSourceContext: NSObject { } self.initializedState = InitializedState(avIoContext: avIoContext, avFormatContext: avFormatContext, audioStream: audioStream, videoStream: videoStream) + + if streamable { + self.fetchedFullDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: (0 ..< Int(Int32.max), .default), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.requestedCompleteFetch = true + } } private func readPacket() -> FFMpegPacket? { diff --git a/TelegramUI/FeedGroupingControllerNode.swift b/TelegramUI/FeedGroupingControllerNode.swift index 9ed6b46406..a2db9c03f6 100644 --- a/TelegramUI/FeedGroupingControllerNode.swift +++ b/TelegramUI/FeedGroupingControllerNode.swift @@ -411,7 +411,7 @@ final class FeedGroupingControllerNode: ViewControllerTracingNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let listInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.right, bottom: insets.bottom, right: layout.safeInsets.left) diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index 911f182234..956066968c 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -8,15 +8,52 @@ import Display import UIKit import AVFoundation -public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedMediaResourceRepresentation) -> Signal { +//let signal = self.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + +public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedStickerAJpegRepresentation { - return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + } } else if let representation = representation as? CachedScaledImageRepresentation { - return fetchCachedScaledImageRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) - } else if let representation = representation as? CachedVideoFirstFrameRepresentation { - return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchCachedScaledImageRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + } + } else if let _ = representation as? CachedVideoFirstFrameRepresentation { + /*return fetchPartialVideoThumbnail(postbox: account.postbox, resource: resource) + |> mapToSignal { data -> Signal in + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + if let data = data, let _ = try? data.write(to: URL(fileURLWithPath: path)) { + return .single(CachedMediaResourceRepresentationResult(temporaryPath: path)) + } else { + return .complete() + } + }*/ + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: data) + } } else if let representation = representation as? CachedScaledVideoFirstFrameRepresentation { - return fetchCachedScaledVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchCachedScaledVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + } } return .never() } @@ -157,7 +194,7 @@ func generateVideoFirstFrame(_ path: String, maxDimensions: CGSize) -> UIImage? } } -private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedVideoFirstFrameRepresentation) -> Signal { +private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData) -> Signal { return Signal { subscriber in if resourceData.complete { let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" @@ -203,7 +240,8 @@ private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource } private func fetchCachedScaledVideoFirstFrameRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedScaledVideoFirstFrameRepresentation) -> Signal { - return account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), complete: true) |> mapToSignal { firstFrame -> Signal in + return account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), complete: true) + |> mapToSignal { firstFrame -> Signal in return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: firstFrame.path), options: [.mappedIfSafe]) { if let image = UIImage(data: data) { diff --git a/TelegramUI/FetchResource.swift b/TelegramUI/FetchResource.swift index a390ee2a60..0210791a26 100644 --- a/TelegramUI/FetchResource.swift +++ b/TelegramUI/FetchResource.swift @@ -3,7 +3,7 @@ import Postbox import TelegramCore import SwiftSignalKit -func fetchResource(account: Account, resource: MediaResource, ranges: Signal) -> Signal? { +func fetchResource(account: Account, resource: MediaResource, intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError>) -> Signal? { return nil } diff --git a/TelegramUI/FetchVideoThumbnail.swift b/TelegramUI/FetchVideoThumbnail.swift new file mode 100644 index 0000000000..6e8ad54145 --- /dev/null +++ b/TelegramUI/FetchVideoThumbnail.swift @@ -0,0 +1,403 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import CoreMedia +import TelegramUIPrivateModule +import Display +import UIKit +import VideoToolbox + +private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + guard let buffer = buffer else { + return 0 + } + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + var bufferPointer = 0 + while bufferPointer < Int(bufferSize) && context.readOffset < context.size { + let bytesRemaining = Int(bufferSize) - bufferPointer + if context.readOffset < context.header.count { + let currentRead = min(bytesRemaining, context.header.count - context.readOffset) + if currentRead != 0 { + context.header.copyBytes(to: buffer.advanced(by: bufferPointer), from: context.readOffset ..< (context.readOffset + currentRead)) + context.readOffset += currentRead + bufferPointer += currentRead + } + } else if context.readOffset >= context.header.count && context.readOffset < context.header.count + context.spacing { + let currentRead = min(bytesRemaining, context.header.count + context.spacing - context.readOffset) + memset(buffer, 0, currentRead) + context.readOffset += currentRead + bufferPointer += currentRead + } else if context.readOffset >= context.header.count + context.spacing { + let normalizedReadOffset = context.readOffset - (context.header.count + context.spacing) + let currentRead = min(bytesRemaining, context.tail.count - normalizedReadOffset) + context.tail.copyBytes(to: buffer.advanced(by: bufferPointer), from: normalizedReadOffset ..< (normalizedReadOffset + currentRead)) + context.readOffset += currentRead + bufferPointer += currentRead + } + } + return Int32(bufferPointer) +} + +private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if (whence & AVSEEK_SIZE) != 0 { + return Int64(context.size) + } else { + context.readOffset = Int(offset) + return offset + } +} + +private final class SoftwareVideoStream { + let index: Int + let fps: CMTime + let timebase: CMTime + let duration: CMTime + let decoder: FFMpegMediaVideoFrameDecoder + let rotationAngle: Double + let aspect: Double + + init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { + self.index = index + self.fps = fps + self.timebase = timebase + self.duration = duration + self.decoder = decoder + self.rotationAngle = rotationAngle + self.aspect = aspect + } +} + +private final class FetchVideoThumbnailSource { + fileprivate let header: Data + fileprivate let spacing: Int + fileprivate let tail: Data + fileprivate var readOffset: Int = 0 + + private var readingError = false + private var videoStream: SoftwareVideoStream? + private var avIoContext: UnsafeMutablePointer? + private var avFormatContext: UnsafeMutablePointer? + fileprivate let size: Int32 + + init(header: Data, spacing: Int, tail: Data) { + let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + self.header = header + self.spacing = spacing + self.tail = tail + + self.size = Int32(header.count + spacing + tail.count) + + var avFormatContextRef = avformat_alloc_context() + guard let avFormatContext = avFormatContextRef else { + self.readingError = true + return + } + + let ioBufferSize = 8 * 1024 + let avIoBuffer = av_malloc(ioBufferSize)! + let avIoContextRef = avio_alloc_context(avIoBuffer.assumingMemoryBound(to: UInt8.self), Int32(ioBufferSize), 0, Unmanaged.passUnretained(self).toOpaque(), readPacketCallback, nil, seekCallback) + self.avIoContext = avIoContextRef + + avFormatContext.pointee.pb = self.avIoContext + + guard avformat_open_input(&avFormatContextRef, nil, nil, nil) >= 0 else { + self.readingError = true + return + } + + guard avformat_find_stream_info(avFormatContext, nil) >= 0 else { + self.readingError = true + return + } + + self.avFormatContext = avFormatContext + + var videoStream: SoftwareVideoStream? + + for streamIndex in FFMpegMediaFrameSourceContextHelpers.streamIndices(formatContext: avFormatContext, codecType: AVMEDIA_TYPE_VIDEO) { + if (avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.disposition & Int32(AV_DISPOSITION_ATTACHED_PIC)) == 0 { + + let codecPar = avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.codecpar! + + if let codec = avcodec_find_decoder(codecPar.pointee.codec_id) { + if let codecContext = avcodec_alloc_context3(codec) { + if avcodec_parameters_to_context(codecContext, avFormatContext.pointee.streams[streamIndex]!.pointee.codecpar) >= 0 { + if avcodec_open2(codecContext, codec, nil) >= 0 { + let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 24)) + + let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale) + + var rotationAngle: Double = 0.0 + if let rotationInfo = av_dict_get(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.metadata, "rotate", nil, 0), let value = rotationInfo.pointee.value { + if strcmp(value, "0") != 0 { + if let angle = Double(String(cString: value)) { + rotationAngle = angle * Double.pi / 180.0 + } + } + } + + let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height) + + videoStream = SoftwareVideoStream(index: streamIndex, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + break + } else { + var codecContextRef: UnsafeMutablePointer? = codecContext + avcodec_free_context(&codecContextRef) + } + } else { + var codecContextRef: UnsafeMutablePointer? = codecContext + avcodec_free_context(&codecContextRef) + } + } + } + } + } + + self.videoStream = videoStream + if self.videoStream == nil { + self.readingError = true + } + } + + deinit { + if let avIoContext = self.avIoContext { + if avIoContext.pointee.buffer != nil { + av_free(avIoContext.pointee.buffer) + } + av_free(avIoContext) + } + if let avFormatContext = self.avFormatContext { + avformat_free_context(avFormatContext) + } + } + + private func readPacketInternal() -> FFMpegPacket? { + guard let avFormatContext = self.avFormatContext else { + return nil + } + + let packet = FFMpegPacket() + if av_read_frame(avFormatContext, &packet.packet) < 0 { + return nil + } else { + return packet + } + } + + func readDecodableFrame() -> MediaTrackDecodableFrame? { + var frames: [MediaTrackDecodableFrame] = [] + + while !self.readingError && frames.isEmpty { + if let packet = self.readPacketInternal() { + if let videoStream = videoStream, Int(packet.packet.stream_index) == videoStream.index { + let avNoPtsRawValue: UInt64 = 0x8000000000000000 + let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue) + let packetPts = packet.packet.pts == avNoPtsValue ? packet.packet.dts : packet.packet.pts + + let pts = CMTimeMake(packetPts, videoStream.timebase.timescale) + let dts = CMTimeMake(packet.packet.dts, videoStream.timebase.timescale) + + let duration: CMTime + + let frameDuration = packet.packet.duration + if frameDuration != 0 { + duration = CMTimeMake(frameDuration * videoStream.timebase.value, videoStream.timebase.timescale) + } else { + duration = videoStream.fps + } + + let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) + frames.append(frame) + } + } else { + self.readingError = true + } + } + + return frames.first + } + + func readFrame() -> (frame: MediaTrackFrame, rotationAngle: CGFloat, aspect: CGFloat)? { + guard let videoStream = self.videoStream else { + return nil + } + guard let decodableFrame = self.readDecodableFrame() else { + return nil + } + guard let decodedFrame = videoStream.decoder.decode(frame: decodableFrame, ptsOffset: nil) else { + return nil + } + + return (decodedFrame, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect)) + } +} + +private final class FetchVideoThumbnailSourceParameters: NSObject { + +} + +private final class FetchVideoThumbnailSourceTimerTarget: NSObject { + @objc func noop() { + } +} + +private final class FetchVideoThumbnailSourceThreadImpl: NSObject { + private var timer: Foundation.Timer + private var disposed = false + + override init() { + self.timer = Foundation.Timer.scheduledTimer(timeInterval: .greatestFiniteMagnitude, target: FetchVideoThumbnailSourceTimerTarget(), selector: #selector(FetchVideoThumbnailSourceTimerTarget.noop), userInfo: nil, repeats: true) + + super.init() + } + + @objc func dispose() { + self.disposed = true + self.timer.invalidate() + } + + @objc func entryPoint() { + RunLoop.current.add(self.timer, forMode: RunLoopMode.defaultRunLoopMode) + while !self.disposed { + if !RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: .distantFuture) { + break + } + } + } + + @objc func fetch(_ parameters: FetchVideoThumbnailSourceParameters) { + print("fetch") + } +} + +private let headerSize = 250 * 1024 +private let tailSize = 16 * 1024 + +func fetchedPartialVideoThumbnailData(postbox: Postbox, fileReference: FileMediaReference) -> Signal { + return Signal { subscriber in + guard let size = fileReference.media.size else { + subscriber.putCompletion() + return EmptyDisposable + } + let fetchedHead = fetchedMediaResource(postbox: postbox, reference: fileReference.resourceReference(fileReference.media.resource), range: (0 ..< min(size, headerSize), .elevated), statsCategory: .video, reportResultStatus: false, preferBackgroundReferenceRevalidation: false).start() + let fetchedTail = fetchedMediaResource(postbox: postbox, reference: fileReference.resourceReference(fileReference.media.resource), range: (max(0, size - tailSize) ..< size, .elevated), statsCategory: .video, reportResultStatus: false, preferBackgroundReferenceRevalidation: false).start() + + return ActionDisposable { + fetchedHead.dispose() + fetchedTail.dispose() + } + } +} + +private func partialVideoThumbnailData(postbox: Postbox, resource: MediaResource) -> Signal<(Data, Int, Data), NoError> { + guard let size = resource.size else { + return .complete() + } + return combineLatest(postbox.mediaBox.resourceData(resource, size: size, in: 0 ..< min(size, headerSize)), postbox.mediaBox.resourceData(resource, size: size, in: max(0, size - tailSize) ..< size)) + |> mapToSignal { header, tail -> Signal<(Data, Int, Data), NoError> in + return .single((header, max(0, size - header.count - tail.count), tail)) + } +} + +private func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage? { + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return nil + } + var maybeImage: CGImage? + if #available(iOSApplicationExtension 9.0, *) { + guard VTCreateCGImageFromCVPixelBuffer(imageBuffer, nil, &maybeImage) == noErr, let image = maybeImage else { + return nil + } + return UIImage(cgImage: image) + } else { + return nil + } + + /*CVPixelBufferLockBaseAddress(imageBuffer, []) + defer { + CVPixelBufferUnlockBaseAddress(imageBuffer, []) + } + + let width = CVPixelBufferGetWidth(imageBuffer) + let height = CVPixelBufferGetHeight(imageBuffer) + guard let yBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)?.assumingMemoryBound(to: UInt8.self) else { + return nil + } + let yPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0) + guard let cbCrBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1)?.assumingMemoryBound(to: UInt8.self) else { + return nil + } + let cbCrPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1) + + let bytesPerPixel = 4 + let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, clear: false) + let rgbBuffer = context.bytes.assumingMemoryBound(to: UInt8.self) + + for y in 0 ..< height { + let rgbBufferLine = rgbBuffer.advanced(by: y * width * bytesPerPixel) + let yBufferLine = yBuffer.advanced(by: y * yPitch) + let cbCrBufferLine = cbCrBuffer.advanced(by: y * 2 * cbCrPitch) + + for x in 0 ..< width { + let y = UInt16(yBufferLine[x]) + let cb = UInt16(cbCrBufferLine[x & ~1]) - 128 + let cr = UInt16(cbCrBufferLine[x | 1]) - 128 + + let rgbOutput = rgbBufferLine.advanced(by: x * bytesPerPixel) + + let r = UInt16(round(Float(y) + Float(cr) * 1.4)) + let g = UInt16(round(Float(y) + Float(cb) * -0.343 + Float(cr) * -0.711)) + let b = UInt16(round(Float(y) + Float(cb) * 1.765)) + + rgbOutput[0] = 0xff + rgbOutput[1] = UInt8(clamping: b > 255 ? 255 : (b < 0 ? 0 : b)) + rgbOutput[2] = UInt8(clamping: g > 255 ? 255 : (g < 0 ? 0 : g)) + rgbOutput[3] = UInt8(clamping: r > 255 ? 255 : (r < 0 ? 0 : r)) + } + } + + + return context.generateImage()*/ +} + + +func fetchPartialVideoThumbnail(postbox: Postbox, resource: MediaResource) -> Signal { + return partialVideoThumbnailData(postbox: postbox, resource: resource) + |> take(1) + |> mapToSignal { header, spacing, tail -> Signal in + return Signal { subscriber in + let source = FetchVideoThumbnailSource(header: header, spacing: spacing, tail: tail) + guard let (frame, rotationAngle, aspect) = source.readFrame() else { + subscriber.putNext(nil) + subscriber.putCompletion() + return EmptyDisposable + } + guard let image = imageFromSampleBuffer(sampleBuffer: frame.sampleBuffer) else { + subscriber.putNext(nil) + subscriber.putCompletion() + return EmptyDisposable + } + guard let data = UIImageJPEGRepresentation(image, 0.7) else { + subscriber.putNext(nil) + subscriber.putCompletion() + return EmptyDisposable + } + subscriber.putNext(data) + subscriber.putCompletion() + return EmptyDisposable + } + } + /*return Signal { subscriber in + let impl = FetchVideoThumbnailSourceThreadImpl() + let thread = Thread(target: impl, selector: #selector(impl.entryPoint), object: nil) + thread.name = "fetchPartialVideoThumbnail" + impl.perform(#selector(impl.fetch(_:)), on: thread, with: FetchVideoThumbnailSourceParameters(), waitUntilDone: false) + thread.start() + + return ActionDisposable { + impl.perform(#selector(impl.dispose), on: thread, with: nil, waitUntilDone: false) + } + }*/ +} diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 3eff758772..0dd43c9a98 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -90,12 +90,6 @@ private let internalMimePrefixes: [String] = [ "image/png" ] -private let supportedVideoMimeTypes = Set([ - "video/mp4", - "video/mpeg4", - "video/mov" -]) - func internalDocumentItemSupportsMimeType(_ type: String, fileName: String?) -> Bool { if let fileName = fileName { let ext = (fileName as NSString).pathExtension @@ -110,9 +104,6 @@ func internalDocumentItemSupportsMimeType(_ type: String, fileName: String?) -> if internalMimeTypes.contains(type) { return true } - if supportedVideoMimeTypes.contains(type) { - return true - } for prefix in internalMimePrefixes { if type.hasPrefix(prefix) { return true @@ -128,7 +119,7 @@ func galleryItemForEntry(account: Account, presentationData: PresentationData, e if let _ = media as? TelegramMediaImage { return ChatImageGalleryItem(account: account, presentationData: presentationData, message: message, location: location) } else if let file = media as? TelegramMediaFile { - if file.isVideo || supportedVideoMimeTypes.contains(file.mimeType) { + if file.isVideo { let content: UniversalVideoContent if file.isAnimated { content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true, enableSound: false) diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index d7f30fd379..f5b38ea6a3 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -393,13 +393,13 @@ final class GridMessageItemNode: GridItemNode { case .Fetching: messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: file) case .Local: - let _ = controllerInteraction.openMessage(message) + let _ = controllerInteraction.openMessage(message, .default) case .Remote: self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file, userInitiated: true).start()) } } } else { - let _ = controllerInteraction.openMessage(message) + let _ = controllerInteraction.openMessage(message, .default) } } case .longTap: diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift index 80c9f84046..f2ed2a4747 100644 --- a/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -159,7 +159,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { insets.left = validLayout.1 insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default(duration: nil)) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -215,7 +215,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/HashtagSearchControllerNode.swift b/TelegramUI/HashtagSearchControllerNode.swift index fa733e73e4..e1fe6c6810 100644 --- a/TelegramUI/HashtagSearchControllerNode.swift +++ b/TelegramUI/HashtagSearchControllerNode.swift @@ -136,7 +136,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index bcb9a6b0e5..5b72ad787d 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -310,7 +310,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: listHeight, height: size.width), insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/InviteContactsControllerNode.swift b/TelegramUI/InviteContactsControllerNode.swift index c6686968ba..31105d73e9 100644 --- a/TelegramUI/InviteContactsControllerNode.swift +++ b/TelegramUI/InviteContactsControllerNode.swift @@ -492,7 +492,7 @@ final class InviteContactsControllerNode: ASDisplayNode { } func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -542,7 +542,7 @@ final class InviteContactsControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: nil) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/IsMediaStreamable.swift b/TelegramUI/IsMediaStreamable.swift new file mode 100644 index 0000000000..a3755b0238 --- /dev/null +++ b/TelegramUI/IsMediaStreamable.swift @@ -0,0 +1,13 @@ +import Foundation +import Postbox +import TelegramCore + +func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Bool { + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + return false + } + if media.isVideo && !media.isAnimated { + return true + } + return false +} diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index d661ec7767..447e5aca5f 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -232,7 +232,7 @@ class ItemListControllerNode: ViewControllerTracingNod if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } var insets = layout.insets(options: [.input]) @@ -436,7 +436,7 @@ class ItemListControllerNode: ViewControllerTracingNod } func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/TelegramUI/LanguageLinkPreviewContentNode.swift b/TelegramUI/LanguageLinkPreviewContentNode.swift new file mode 100644 index 0000000000..e742ab283b --- /dev/null +++ b/TelegramUI/LanguageLinkPreviewContentNode.swift @@ -0,0 +1,102 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +final class LanguageLinkPreviewContentNode: ASDisplayNode, ShareContentContainerNode { + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + private let titleNode: ImmediateTextNode + private let textNode: ImmediateTextNode + + init(account: Account, localizationInfo: LocalizationInfo, theme: PresentationTheme, strings: PresentationStrings, openTranslationUrl: @escaping (String) -> Void) { + self.titleNode = ImmediateTextNode() + self.titleNode.textAlignment = .center + + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 0 + self.textNode.textAlignment = .center + self.textNode.lineSpacing = 0.1 + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.titleNode.attributedText = NSAttributedString(string: "Change Language?", font: Font.medium(20.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + + let completionScore = localizationInfo.translatedStringCount * 100 / max(1, localizationInfo.totalStringCount) + + let text: String + if localizationInfo.translatedStringCount == 0 { + self.titleNode.isHidden = true + text = "This language is not available yet." + } else { + text = "You are about to apply a custom language pack \(localizationInfo.title) that is \(completionScore)% complete.\nThis will translate the entire interface. You can suggest corrections in the [translation panel](https://translations.telegram.org/\(localizationInfo.languageCode)/).\nYou can change your language back at any time in Settings." + } + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.actionSheet.primaryTextColor) + let link = MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.actionSheet.controlAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""]) + + self.textNode.attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center) + self.textNode.linkHighlightColor = theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedStringKey(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + self.textNode.tapAttributeAction = { attributes in + if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] { + openTranslationUrl("https://translations.telegram.org/\(localizationInfo.languageCode)/") + } + } + + //self.textNode.attributedText = NSAttributedString(string: "", font: Font.regular(15.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + } + + func activate() { + } + + func deactivate() { + } + + func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { + } + + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let insets = UIEdgeInsets(top: 12.0, left: 10.0, bottom: 12.0 + bottomInset, right: 10.0) + let titleSpacing: CGFloat = 7.0 + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: .greatestFiniteMagnitude)) + let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: .greatestFiniteMagnitude)) + + let nodeHeight: CGFloat + if !self.titleNode.isHidden { + nodeHeight = titleSize.height + titleSpacing + textSize.height + insets.top + insets.bottom + } else { + nodeHeight = textSize.height + insets.top + insets.bottom + } + + let verticalOrigin = size.height - nodeHeight + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + insets.top), size: titleSize)) + let textOrigin: CGFloat + if !self.titleNode.isHidden { + textOrigin = verticalOrigin + insets.top + titleSize.height + titleSpacing + } else { + textOrigin = verticalOrigin + insets.top + } + + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: textOrigin), size: textSize)) + + self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) + } + + func updateSelectedPeers() { + } +} diff --git a/TelegramUI/LanguageLinkPreviewController.swift b/TelegramUI/LanguageLinkPreviewController.swift new file mode 100644 index 0000000000..73c8ca8a5d --- /dev/null +++ b/TelegramUI/LanguageLinkPreviewController.swift @@ -0,0 +1,118 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +public final class LanguageLinkPreviewController: ViewController { + private var controllerNode: LanguageLinkPreviewControllerNode { + return self.displayNode as! LanguageLinkPreviewControllerNode + } + + private var animatedIn = false + + private let account: Account + private let identifier: String + private var localizationInfo: LocalizationInfo? + private var presentationData: PresentationData + + private let disposable = MetaDisposable() + + public init(account: Account, identifier: String) { + self.account = account + self.identifier = identifier + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = LanguageLinkPreviewControllerNode(account: self.account, requestLayout: { [weak self] transition in + self?.requestLayout(transition: transition) + }, openUrl: { [weak self] url in + guard let strongSelf = self else { + return + } + openExternalUrl(account: strongSelf.account, url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: nil, dismissInput: { + }) + }) + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + self.controllerNode.activate = { [weak self] in + self?.activate() + } + self.displayNodeDidLoad() + + self.disposable.set((requestLocalizationPreview(postbox: self.account.postbox, network: self.account.network, identifier: self.identifier) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.localizationInfo = result + strongSelf.controllerNode.setData(localizationInfo: result) + }, error: { [weak self] _ in + self?.dismiss() + })) + self.ready.set(self.controllerNode.ready.get()) + } + + override public func loadView() { + super.loadView() + + self.statusBar.removeFromSupernode() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + private func activate() { + guard let localizationInfo = self.localizationInfo else { + return + } + self.controllerNode.setInProgress(true) + self.disposable.set((downoadAndApplyLocalization(postbox: self.account.postbox, network: self.account.network, languageCode: localizationInfo.languageCode) + |> deliverOnMainQueue).start(error: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.setInProgress(false) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.setInProgress(false) + strongSelf.dismiss() + })) + } +} diff --git a/TelegramUI/LanguageLinkPreviewControllerNode.swift b/TelegramUI/LanguageLinkPreviewControllerNode.swift new file mode 100644 index 0000000000..5a03b69106 --- /dev/null +++ b/TelegramUI/LanguageLinkPreviewControllerNode.swift @@ -0,0 +1,473 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let account: Account + private var presentationData: PresentationData + + private let requestLayout: (ContainedViewLayoutTransition) -> Void + private let openUrl: (String) -> Void + + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? + + private let dimNode: ASDisplayNode + + private let wrappingScrollNode: ASScrollNode + private let cancelButtonNode: ASButtonNode + + private let contentContainerNode: ASDisplayNode + private let contentBackgroundNode: ASImageNode + + private var contentNode: (ASDisplayNode & ShareContentContainerNode)? + private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? + private var animateContentNodeOffsetFromBackgroundOffset: CGFloat? + + private let actionsBackgroundNode: ASImageNode + private let actionButtonNode: ShareActionButtonNode + private let actionIndicator: ActivityIndicator + private let actionSeparatorNode: ASDisplayNode + + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + var activate: (() -> Void)? + + let ready = Promise() + private var didSetReady = false + + private var scheduledLayoutTransitionRequestId: Int = 0 + private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + + private let disposable = MetaDisposable() + + init(account: Account, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, openUrl: @escaping (String) -> Void) { + self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.requestLayout = requestLayout + self.openUrl = openUrl + + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) + + let theme = self.presentationData.theme + let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.cancelButtonNode = ASButtonNode() + self.cancelButtonNode.displaysAsynchronously = false + self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) + self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + self.contentContainerNode.clipsToBounds = true + + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + self.contentBackgroundNode.image = roundedBackground + + self.actionsBackgroundNode = ASImageNode() + self.actionsBackgroundNode.isLayerBacked = true + self.actionsBackgroundNode.displayWithoutProcessing = true + self.actionsBackgroundNode.displaysAsynchronously = false + self.actionsBackgroundNode.image = halfRoundedBackground + + self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + self.actionButtonNode.displaysAsynchronously = false + self.actionButtonNode.titleNode.displaysAsynchronously = false + self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + + self.actionIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, 22.0, 1.0, false)) + self.actionIndicator.isHidden = true + + self.actionSeparatorNode = ASDisplayNode() + self.actionSeparatorNode.isLayerBacked = true + self.actionSeparatorNode.displaysAsynchronously = false + self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + + self.wrappingScrollNode.addSubnode(self.cancelButtonNode) + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + + self.actionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside) + + self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.addSubnode(self.actionSeparatorNode) + self.contentContainerNode.addSubnode(self.actionsBackgroundNode) + self.contentContainerNode.addSubnode(self.actionButtonNode) + self.contentContainerNode.addSubnode(self.actionIndicator) + + self.transitionToContentNode(ShareLoadingContainerNode(theme: theme)) + + self.actionButtonNode.alpha = 0.0 + self.actionSeparatorNode.alpha = 0.0 + self.actionsBackgroundNode.alpha = 0.0 + + self.ready.set(.single(true)) + self.didSetReady = true + } + + deinit { + self.disposable.dispose() + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + + func transitionToContentNode(_ contentNode: (ASDisplayNode & ShareContentContainerNode)?, fastOut: Bool = false) { + if self.contentNode !== contentNode { + let transition: ContainedViewLayoutTransition + + let previous = self.contentNode + if let previous = previous { + previous.setContentOffsetUpdated(nil) + transition = .animated(duration: 0.4, curve: .spring) + + self.previousContentNode = previous + previous.alpha = 0.0 + previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: fastOut ? 0.1 : 0.2, removeOnCompletion: true, completion: { [weak self, weak previous] _ in + if let strongSelf = self, let previous = previous { + if strongSelf.previousContentNode === previous { + strongSelf.previousContentNode = nil + } + previous.removeFromSupernode() + } + }) + } else { + transition = .immediate + } + self.contentNode = contentNode + + if let (layout, navigationBarHeight, bottomGridInset) = self.containerLayout { + if let contentNode = contentNode, let previous = previous { + contentNode.frame = previous.frame + contentNode.updateLayout(size: previous.bounds.size, bottomInset: bottomGridInset, transition: .immediate) + + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + + contentNode.alpha = 1.0 + let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.35) + animation.fillMode = kCAFillModeBoth + if !fastOut { + animation.beginTime = CACurrentMediaTime() + 0.1 + } + contentNode.layer.add(animation, forKey: "opacity") + + self.animateContentNodeOffsetFromBackgroundOffset = self.contentBackgroundNode.frame.minY + self.scheduleInteractiveTransition(transition) + + contentNode.activate() + previous.deactivate() + } else { + if let contentNode = self.contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + } + + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } else if let contentNode = contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + } + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + let titleAreaHeight: CGFloat = 64.0 + + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + + let width = min(layout.size.width, layout.size.height) - 20.0 + let sideInset = floor((layout.size.width - width) / 2.0) + + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0) + + let bottomGridInset = buttonHeight + + self.containerLayout = (layout, navigationBarHeight, bottomGridInset) + self.scheduledLayoutTransitionRequest = nil + + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + + transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset))) + + let actionButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)) + transition.updateFrame(node: self.actionButtonNode, frame: actionButtonFrame) + let indicatorSize = self.actionIndicator.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.actionIndicator, frame: CGRect(origin: CGPoint(x: actionButtonFrame.minX + 12.0, y: actionButtonFrame.minY + floor((actionButtonFrame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + + transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) + contentNode.updateLayout(size: gridSize, bottomInset: bottomGridInset, transition: transition) + } + } + + private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { + if let (layout, _, _) = self.containerLayout { + var insets = layout.insets(options: [.statusBar, .input]) + insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + + let width = min(layout.size.width, layout.size.height) - 20.0 + + let sideInset = floor((layout.size.width - width) / 2.0) + + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - contentOffset), size: contentFrame.size) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + if backgroundFrame.maxY > contentFrame.maxY { + backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY + } + if backgroundFrame.size.height < buttonHeight + 32.0 { + backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height + backgroundFrame.size.height = buttonHeight + 32.0 + } + transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + + if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset { + self.animateContentNodeOffsetFromBackgroundOffset = nil + let offset = backgroundFrame.minY - animateContentNodeOffsetFromBackgroundOffset + if let contentNode = self.contentNode { + transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: 0.0, y: -offset)) + } + if let previousContentNode = self.previousContentNode { + transition.updatePosition(node: previousContentNode, position: previousContentNode.position.offsetBy(dx: 0.0, dy: offset)) + } + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancelButtonPressed() + } + } + + @objc func cancelButtonPressed() { + self.cancel?() + } + + @objc func installActionButtonPressed() { + self.activate?() + } + + func animateIn() { + if self.contentNode != nil { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func animateOut(completion: (() -> Void)? = nil) { + if self.contentNode != nil { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } else { + self.dismiss?() + completion?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) { + return result + } + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + private func scheduleInteractiveTransition(_ transition: ContainedViewLayoutTransition) { + if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { + switch scheduledLayoutTransitionRequest.1 { + case .immediate: + self.scheduleLayoutTransitionRequest(transition) + default: + break + } + } else { + self.scheduleLayoutTransitionRequest(transition) + } + } + + private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { + let requestId = self.scheduledLayoutTransitionRequestId + self.scheduledLayoutTransitionRequestId += 1 + self.scheduledLayoutTransitionRequest = (requestId, transition) + (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in + if let strongSelf = self { + if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { + strongSelf.scheduledLayoutTransitionRequest = nil + strongSelf.requestLayout(currentRequestTransition) + } + } + }) + self.setNeedsLayout() + } + + func transitionToProgress(signal: Signal) { + let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme), fastOut: true) + let timestamp = CACurrentMediaTime() + self.disposable.set(signal.start(completed: { [weak self] in + let minDelay = 0.6 + let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) + Queue.mainQueue().after(delay, { + if let strongSelf = self { + strongSelf.cancel?() + } + }) + })) + } + + func setData(localizationInfo: LocalizationInfo) { + let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 1.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 1.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 1.0) + + if localizationInfo.translatedStringCount == 0 { + self.actionButtonNode.isEnabled = false + self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_ApplyLocalization, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_ApplyLocalization, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .disabled) + + } else { + self.actionButtonNode.isEnabled = true + self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_ApplyLocalization, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_ApplyLocalization, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .disabled) + } + + self.transitionToContentNode(LanguageLinkPreviewContentNode(account: self.account, localizationInfo: localizationInfo, theme: self.presentationData.theme, strings: self.presentationData.strings, openTranslationUrl: { [weak self] url in + self?.openUrl(url) + })) + } + + func setInProgress(_ value: Bool) { + self.actionIndicator.isHidden = !value + self.actionButtonNode.isEnabled = !value + } +} diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 976a1ee9c4..f2f5f2c1f1 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -775,7 +775,7 @@ final class ListMessageFileItemNode: ListMessageNode { } case .Local: if let item = self.item, let controllerInteraction = self.controllerInteraction { - let _ = controllerInteraction.openMessage(item.message) + let _ = controllerInteraction.openMessage(item.message, .default) } } case .playbackStatus: diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 8de7ad408d..1e196e9afd 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -478,14 +478,14 @@ final class ListMessageSnippetItemNode: ListMessageNode { 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) { + if !item.controllerInteraction.openMessage(item.message, .default) { item.controllerInteraction.openInstantPage(item.message) } } else { item.controllerInteraction.openInstantPage(item.message) } } else { - if !item.controllerInteraction.openMessage(item.message) { + if !item.controllerInteraction.openMessage(item.message, .default) { item.controllerInteraction.openUrl(currentPrimaryUrl, false, false) } } @@ -535,7 +535,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { if case .longTap = gesture { item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url)) } else if url == self.currentPrimaryUrl { - if !item.controllerInteraction.openMessage(item.message) { + if !item.controllerInteraction.openMessage(item.message, .default) { item.controllerInteraction.openUrl(url, false, false) } } else { diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index 163c3fbae8..390abb454c 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -579,9 +579,12 @@ private final class AudioPlayerRendererContext { } if let requestingFramesContext = self.requestingFramesContext { - requestingFramesContext.queue.async { + requestingFramesContext.queue.async { [weak self] in let takenFrame = requestingFramesContext.takeFrame() audioPlayerRendererQueue.async { + guard let strongSelf = self else { + return + } switch takenFrame { case let .frame(frame): if let dataBuffer = CMSampleBufferGetDataBuffer(frame.sampleBuffer) { @@ -593,10 +596,10 @@ private final class AudioPlayerRendererContext { let bytes = malloc(takeLength)! CMBlockBufferCopyDataBytes(dataBuffer, 0, takeLength, bytes) - self.enqueueSamples(Data(bytesNoCopy: bytes.assumingMemoryBound(to: UInt8.self), count: takeLength, deallocator: .free), sampleIndex: bufferSampleIndex) + strongSelf.enqueueSamples(Data(bytesNoCopy: bytes.assumingMemoryBound(to: UInt8.self), count: takeLength, deallocator: .free), sampleIndex: bufferSampleIndex) if takeLength < dataLength { - self.bufferContext.with { context in + strongSelf.bufferContext.with { context in let copyOffset = context.overflowData.count context.overflowData.count += dataLength - takeLength context.overflowData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in @@ -605,15 +608,15 @@ private final class AudioPlayerRendererContext { } } - self.checkBuffer() + strongSelf.checkBuffer() } else { assertionFailure() } case .skipFrame: - self.checkBuffer() + strongSelf.checkBuffer() break case .noFrames, .finished: - self.requestingFramesContext = nil + strongSelf.requestingFramesContext = nil } } } diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift index db1cca04ed..159ebefda4 100644 --- a/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -172,7 +172,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { insets.left = validLayout.1 insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default(duration: nil)) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -237,7 +237,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/NotificationExceptions.swift b/TelegramUI/NotificationExceptions.swift index ea469d975f..00573fb257 100644 --- a/TelegramUI/NotificationExceptions.swift +++ b/TelegramUI/NotificationExceptions.swift @@ -1002,7 +1002,7 @@ private final class NotificationExceptionsSearchControllerContentNode: SearchDis if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } self.listNode.containerLayoutUpdated(layout, navigationBarHeight: 0, transition: transition) diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 6afb788ad3..9bf1194acd 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -423,7 +423,7 @@ private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSet entries.append(.messageAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.privateChats.enabled)) entries.append(.messagePreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.privateChats.displayPreviews)) entries.append(.messageSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.privateChats.sound)), filteredGlobalSound(globalSettings.privateChats.sound))) - entries.append(.userExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.0)) + //entries.append(.userExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.0)) entries.append(.messageNotice(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsHelp)) entries.append(.groupHeader(presentationData.theme, presentationData.strings.Notifications_GroupNotifications)) diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift index 87e1cc814b..ab2a35cc24 100644 --- a/TelegramUI/OpenResolvedUrl.swift +++ b/TelegramUI/OpenResolvedUrl.swift @@ -20,7 +20,7 @@ private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatContr } } -func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: OpenURLContext = .generic, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, present: (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) { +func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: OpenURLContext = .generic, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)? = nil, present: (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) { switch resolvedUrl { case let .externalUrl(url): openExternalUrl(account: account, context: context, url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: navigationController, dismissInput: dismissInput) @@ -58,7 +58,9 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: Open openPeer(peerId, .chat(textInputState: nil, messageId: messageId)) case let .stickerPack(name): dismissInput() - present(StickerPackPreviewController(account: account, stickerPack: .name(name), parentNavigationController: navigationController), nil) + let controller = StickerPackPreviewController(account: account, stickerPack: .name(name), parentNavigationController: navigationController) + controller.sendSticker = sendFile + present(controller, nil) case let .instantView(webpage, anchor): navigationController?.pushViewController(InstantPageController(account: account, webPage: webpage, anchor: anchor)) case let .join(link): @@ -66,6 +68,9 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: Open present(JoinLinkPreviewController(account: account, link: link, navigateToPeer: { peerId in openPeer(peerId, .chat(textInputState: nil, messageId: nil)) }), nil) + case let .localization(identifier): + dismissInput() + present(LanguageLinkPreviewController(account: account, identifier: identifier), nil) case let .proxy(host, port, username, password, secret): let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let server: ProxyServerSettings diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 828838cb1c..e3e1196028 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -230,6 +230,22 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic convertedUrl = "https://t.me/addstickers/\(set)" } } + } else if parsedUrl.host == "setlanguage" { + if let components = URLComponents(string: "/?" + query) { + var lang: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "lang" { + lang = value + } + } + } + } + if let lang = lang { + convertedUrl = "https://t.me/setlanguage/\(lang)" + } + } } else if parsedUrl.host == "msg_url" { if let components = URLComponents(string: "/?" + query) { var shareUrl: String? diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index ddf67e39d9..694fbb3b66 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -48,7 +48,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec } var openMessageImpl: ((MessageId) -> Bool)? - self.controllerInteraction = ChatControllerInteraction(openMessage: { message in + self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in if let openMessageImpl = openMessageImpl { return openMessageImpl(message.id) } else { @@ -232,13 +232,13 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: listViewCurve) self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) if let replacementHistoryNode = replacementHistoryNode { - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) replacementHistoryNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) } } @@ -424,7 +424,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) } self.replacementHistoryNodeReadyDisposable.set((historyNode.historyState.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in @@ -507,7 +507,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec self.historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) self.historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) self.historyNode.recursivelyEnsureDisplaySynchronously(true) diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 9c67104545..3d2d7d541b 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -78,7 +78,7 @@ public class PeerMediaCollectionController: TelegramController { } }) - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, _ in if let strongSelf = self, strongSelf.isNodeLoaded, let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id) { guard let navigationController = strongSelf.navigationController as? NavigationController else { return false diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index fede97471e..9cc6f4f116 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -319,7 +319,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } var additionalBottomInset: CGFloat = 0.0 @@ -424,7 +424,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { additionalBottomInset = selectionPanel.bounds.size.height } - node.updateLayout(transition: .immediate, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: containerLayout.0.size, insets: UIEdgeInsets(top: insets.top, left: insets.right + containerLayout.0.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + containerLayout.0.safeInsets.left), duration: 0.0, curve: .Default)) + node.updateLayout(transition: .immediate, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: containerLayout.0.size, insets: UIEdgeInsets(top: insets.top, left: insets.right + containerLayout.0.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + containerLayout.0.safeInsets.left), duration: 0.0, curve: .Default(duration: nil))) let historyEmptyNode = PeerMediaCollectionEmptyNode(mode: mediaCollectionInterfaceState.mode, theme: self.mediaCollectionInterfaceState.theme, strings: self.mediaCollectionInterfaceState.strings) historyEmptyNode.isHidden = true diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index 2054cb932a..2eb4c5fc3b 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -152,7 +152,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 57bbc728ec..bcc18993d6 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -165,9 +165,12 @@ private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: let fullSizeResource: MediaResource = fileReference.media.resource - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false) + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false, fetch: false) + let fetchedFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false, fetch: true) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in if maybeData.complete { return .single((nil, maybeData.path, true)) } else { @@ -195,17 +198,20 @@ private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: thumbnail = .single(nil) } - let fullSizeDataAndPath = maybeFullSize |> map { next -> (String?, Bool) in + let fullSizeDataAndPath = fetchedFullSize + |> map { next -> (String?, Bool) in return (next.size == 0 ? nil : next.path, next.complete) } - return thumbnail |> mapToSignal { thumbnailData in - return fullSizeDataAndPath |> map { (dataPath, complete) in + return thumbnail + |> mapToSignal { thumbnailData in + return fullSizeDataAndPath + |> map { (dataPath, complete) in return (thumbnailData, dataPath, complete) } } } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } |> filter({ $0.0 != nil || $0.1 != nil }) return signal } @@ -215,7 +221,8 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef let thumbnailResource = smallestRepresentation.resource let fullSizeResource = fileReference.media.resource - let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false) + let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false) + let fetchedFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: true) let signal = maybeFullSize |> take(1) @@ -239,14 +246,27 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef } } - - let fullSizeDataAndPath = maybeFullSize |> map { next -> ((Data, String)?, Bool) in + let fullSizeDataAndPath = Signal { subscriber in + let dataDisposable = fetchedFullSize.start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + }) + //let fetchedDisposable = fetchedPartialVideoThumbnailData(postbox: postbox, fileReference: fileReference).start() + return ActionDisposable { + dataDisposable.dispose() + //fetchedDisposable.dispose() + } + } + |> map { next -> ((Data, String)?, Bool) in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) return (data == nil ? nil : (data!, next.path), next.complete) } - return thumbnail |> mapToSignal { thumbnailData in - return fullSizeDataAndPath |> map { (dataAndPath, complete) in + return thumbnail + |> mapToSignal { thumbnailData in + return fullSizeDataAndPath + |> map { (dataAndPath, complete) in return (thumbnailData, dataAndPath, complete) } } @@ -667,7 +687,8 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: Im let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) { - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: onlyFullSize) + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: onlyFullSize, fetch: false) + let fetchedFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: onlyFullSize, fetch: true) let signal = maybeFullSize |> take(1) @@ -690,19 +711,21 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: Im } } - let fullSizeData: Signal<(Data?, Bool), NoError> = maybeFullSize - |> map { next -> (Data?, Bool) in - return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) - } + let fullSizeData: Signal<(Data?, Bool), NoError> = fetchedFullSize + |> map { next -> (Data?, Bool) in + return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } - - return thumbnail |> mapToSignal { thumbnailData in - return fullSizeData |> map { (fullSizeData, complete) in + return thumbnail + |> mapToSignal { thumbnailData in + return fullSizeData + |> map { (fullSizeData, complete) in return (thumbnailData, fullSizeData, complete) } } } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } + |> filter({ $0.0 != nil || $0.1 != nil }) return signal } else { diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 7572131bf4..e834c5be40 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -117,7 +117,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if case .UnreadEntry = entry { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) break } index -= 1 @@ -127,7 +127,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.index >= unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) break } index -= 1 @@ -138,7 +138,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = 0 for entry in toView.filteredEntries.reversed() { if entry.index < unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) break } index += 1 @@ -148,7 +148,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .top(relativeOffset), animated: false, curve: .Default, directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .top(relativeOffset), animated: false, curve: .Default(duration: nil), directionHint: .Down) break } index -= 1 @@ -158,7 +158,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = 0 for entry in toView.filteredEntries.reversed() { if entry.index < scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default, directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) break } index += 1 @@ -171,7 +171,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if scrollIndex.isLessOrEqual(to: entry.index) { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index -= 1 @@ -181,7 +181,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = 0 for entry in toView.filteredEntries.reversed() { if !scrollIndex.isLess(than: entry.index) { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index += 1 diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 2b6616617e..4c002ec1ae 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -110,6 +110,7 @@ enum PresentationResourceKey: Int32 { case chatBubbleActionButtonOutgoingBottomRightImage case chatBubbleActionButtonOutgoingBottomSingleImage + case chatBubbleFileCloudFetchMediaIcon case chatBubbleFileCloudFetchIncomingIcon case chatBubbleFileCloudFetchOutgoingIcon case chatBubbleFileCloudFetchedIncomingIcon diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 9c25036f3d..afa4a335f8 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -1012,6 +1012,18 @@ struct PresentationResourcesChat { }) } + static func chatBubbleFileCloudFetchMediaIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleFileCloudFetchMediaIcon.rawValue, { theme in + guard let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.chat.bubble.mediaOverlayControlForegroundColor) else { + return nil + } + return generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: image.size)) + }) + }) + } + static func chatBubbleFileCloudFetchIncomingIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleFileCloudFetchIncomingIcon.rawValue, { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.chat.bubble.incomingAccentControlColor) diff --git a/TelegramUI/RadialProgressContentNode.swift b/TelegramUI/RadialProgressContentNode.swift index bce06cbd73..a65349cbc1 100644 --- a/TelegramUI/RadialProgressContentNode.swift +++ b/TelegramUI/RadialProgressContentNode.swift @@ -271,7 +271,7 @@ final class RadialProgressContentNode: RadialStatusContentNode { } override func enqueueReadyForTransition(_ f: @escaping () -> Void) { - if self.spinnerNode.isAnimatingProgress { + if self.spinnerNode.isAnimatingProgress && self.progress == 1.0 { self.enqueuedReadyForTransition = f } else { f() diff --git a/TelegramUI/SecureIdLocalResource.swift b/TelegramUI/SecureIdLocalResource.swift index e5fb2d4c37..603ae015bf 100644 --- a/TelegramUI/SecureIdLocalResource.swift +++ b/TelegramUI/SecureIdLocalResource.swift @@ -68,7 +68,7 @@ func fetchSecureIdLocalImageResource(postbox: Postbox, resource: SecureIdLocalIm subscriber.putNext(.reset) - let fetch = fetchResource(resource.source, .single(IndexSet(integersIn: 0 ..< Int.max)), nil) + let fetch = fetchResource(resource.source, .single([(0 ..< Int.max, .default)]), nil) let buffer = Atomic(value: Buffer()) let disposable = fetch.start(next: { result in switch result { diff --git a/TelegramUI/StickerPreviewPeekContent.swift b/TelegramUI/StickerPreviewPeekContent.swift index d047cccc7f..73029c1f7d 100644 --- a/TelegramUI/StickerPreviewPeekContent.swift +++ b/TelegramUI/StickerPreviewPeekContent.swift @@ -75,7 +75,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) break } - self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false)) + self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false, fetched: true)) super.init() diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 4e58f3117f..4ae81799c6 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -48,7 +48,7 @@ private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, let thumbnailResource = chatMessageStickerResource(file: file, small: true) let resource = chatMessageStickerResource(file: file, small: small) - let maybeFetched = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil), complete: false) + let maybeFetched = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil), complete: false, fetch: false) return maybeFetched |> take(1) @@ -64,45 +64,31 @@ private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) } - if fetched { - return Signal { subscriber in - let fetch = fetchedMediaResource(postbox: account.postbox, reference: stickerPackFileReference(file).resourceReference(resource)).start() - let disposable = (fullSizeData |> map { (data, complete) -> (Data?, Data?, Bool) in - return (nil, data, complete) - }).start(next: { next in - subscriber.putNext(next) - }, error: { error in - subscriber.putError(error) - }, completed: { - subscriber.putCompletion() - }) - - return ActionDisposable { - fetch.dispose() - disposable.dispose() - } + return Signal { subscriber in + var fetch: Disposable? + if fetched { + fetch = fetchedMediaResource(postbox: account.postbox, reference: stickerPackFileReference(file).resourceReference(resource)).start() } - } else { - return Signal { subscriber in - var fetchThumbnail: Disposable? - if !thumbnailResource.id.isEqual(to: resource.id) { - fetchThumbnail = fetchedMediaResource(postbox: account.postbox, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() - } - let disposable = (combineLatest(thumbnailData, fullSizeData) - |> map { thumbnailData, fullSizeData -> (Data?, Data?, Bool) in - return (thumbnailData.complete ? try? Data(contentsOf: URL(fileURLWithPath: thumbnailData.path)) : nil, fullSizeData.0, fullSizeData.1) - }).start(next: { next in - subscriber.putNext(next) - }, error: { error in - subscriber.putError(error) - }, completed: { - subscriber.putCompletion() - }) - - return ActionDisposable { - fetchThumbnail?.dispose() - disposable.dispose() - } + + var fetchThumbnail: Disposable? + if !thumbnailResource.id.isEqual(to: resource.id) { + fetchThumbnail = fetchedMediaResource(postbox: account.postbox, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() + } + let disposable = (combineLatest(thumbnailData, fullSizeData) + |> map { thumbnailData, fullSizeData -> (Data?, Data?, Bool) in + return (thumbnailData.complete ? try? Data(contentsOf: URL(fileURLWithPath: thumbnailData.path)) : nil, fullSizeData.0, fullSizeData.1) + }).start(next: { next in + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + fetch?.dispose() + fetchThumbnail?.dispose() + disposable.dispose() } } } diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 823dd85c3d..4cb82d3cc6 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -90,7 +90,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.containerNode = ASDisplayNode() self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in + self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil diff --git a/TelegramUI/TwoStepVerificationPasswordEntryController.swift b/TelegramUI/TwoStepVerificationPasswordEntryController.swift index 20358d0865..d39875fa7d 100644 --- a/TelegramUI/TwoStepVerificationPasswordEntryController.swift +++ b/TelegramUI/TwoStepVerificationPasswordEntryController.swift @@ -328,8 +328,8 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV $0.withUpdatedUpdating(false) } switch update { - case let .password(password, pendingEmailPattern): - result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmailPattern: pendingEmailPattern))) + case let .password(password, pendingEmail): + result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmailPattern: pendingEmail?.pattern))) case .none: break } @@ -353,8 +353,8 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV $0.withUpdatedUpdating(false) } switch update { - case let .password(password, pendingEmailPattern): - result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmailPattern: pendingEmailPattern))) + case let .password(password, pendingEmail): + result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmailPattern: pendingEmail?.pattern))) case .none: break } diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index 907f65e405..7aee778307 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -14,6 +14,7 @@ private enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) case stickerPack(String) case join(String) + case localization(String) case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) } @@ -32,6 +33,7 @@ enum ResolvedUrl { case instantView(TelegramMediaWebpage, String?) case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) case join(String) + case localization(String) } private func parseInternalUrl(query: String) -> ParsedInternalUrl? { @@ -94,6 +96,8 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .stickerPack(pathComponents[1]) } else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" { return .join(pathComponents[1]) + } else if pathComponents[0] == "setlanguage" { + return .localization(pathComponents[1]) } else if let value = Int(pathComponents[1]) { return .peerName(peerName, .channelMessage(Int32(value))) } else { @@ -111,29 +115,31 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig switch url { case let .peerName(name, parameter): return resolvePeerByName(account: account, name: name) - |> take(1) - |> map { peerId -> ResolvedUrl? in - if let peerId = peerId { - if let parameter = parameter { - switch parameter { - case let .botStart(payload): - return .botStart(peerId: peerId, payload: payload) - case let .groupBotStart(payload): - return .groupBotStart(peerId: peerId, payload: payload) - case let .channelMessage(id): - return .channelMessage(peerId: peerId, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)) - } - } else { - return .peer(peerId, .chat(textInputState: nil, messageId: nil)) + |> take(1) + |> map { peerId -> ResolvedUrl? in + if let peerId = peerId { + if let parameter = parameter { + switch parameter { + case let .botStart(payload): + return .botStart(peerId: peerId, payload: payload) + case let .groupBotStart(payload): + return .groupBotStart(peerId: peerId, payload: payload) + case let .channelMessage(id): + return .channelMessage(peerId: peerId, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)) } } else { - return nil + return .peer(peerId, .chat(textInputState: nil, messageId: nil)) } + } else { + return nil } + } case let .stickerPack(name): return .single(.stickerPack(name: name)) case let .join(link): return .single(.join(link)) + case let .localization(identifier): + return .single(.localization(identifier)) case let .proxy(host, port, username, password, secret): return .single(.proxy(host: host, port: port, username: username, password: password, secret: secret)) } diff --git a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift index d1e392d93e..c5593558ac 100644 --- a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift @@ -210,7 +210,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex insets.left = validLayout.1 insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default(duration: nil)) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -271,7 +271,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex if curve == 7 { listViewCurve = .Spring(duration: duration) } else { - listViewCurve = .Default + listViewCurve = .Default(duration: duration) } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve)