diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000000..6f9f00ff49 --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/.gitignore b/.gitignore index cb0aa6a14b..a9eb99ef70 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ build-input/* submodules/OpusBinding/SharedHeaders/* submodules/FFMpegBinding/SharedHeaders/* submodules/OpenSSLEncryptionProvider/SharedHeaders/* -buildServer.json +submodules/TelegramCore/FlatSerialization/Sources/* +buildServer.json \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index ac9d05195d..9c7dc87aa6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,9 +11,6 @@ url=https://github.com/bazelbuild/rules_swift.git [submodule "build-system/bazel-rules/apple_support"] path = build-system/bazel-rules/apple_support url = https://github.com/bazelbuild/apple_support.git -[submodule "submodules/TgVoip/libtgvoip"] - path = submodules/TgVoip/libtgvoip - url = https://github.com/telegramdesktop/libtgvoip.git [submodule "submodules/TgVoipWebrtc/tgcalls"] path = submodules/TgVoipWebrtc/tgcalls url=../tgcalls.git diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 8fb66cee92..b09b444e2c 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -32,7 +32,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private var originalContent: BrowserContent? private let url: String - private var webPage: TelegramMediaWebpage? + private var webPage: (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? let uuid: UUID @@ -97,7 +97,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg init(context: AccountContext, presentationData: PresentationData, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation, preloadedResouces: [Any]?, originalContent: BrowserContent? = nil) { self.context = context - self.webPage = webPage + var instantPage: InstantPage? + if case let .Loaded(content) = webPage.content { + instantPage = content.instantPage?._parse() + } + self.webPage = (webPage, instantPage) self.presentationData = presentationData self.theme = instantPageThemeForType(presentationData.theme.overallDarkAppearance ? .dark : .light, settings: .defaultSettings) self.sourceLocation = sourceLocation @@ -267,7 +271,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) { - if self.webPage != webPage { + if self.webPage?.webPage != webPage { if self.webPage != nil && self.currentLayout != nil { if let snapshotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.scrollNode.frame @@ -279,7 +283,15 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } self.setupScrollOffsetOnLayout = self.webPage == nil - self.webPage = webPage + if let webPage { + var instantPage: InstantPage? + if case let .Loaded(content) = webPage.content { + instantPage = content.instantPage?._parse() + } + self.webPage = (webPage, instantPage) + } else { + self.webPage = nil + } if let anchor = anchor { self.initialAnchor = anchor.removingPercentEncoding } else if let state = state { @@ -455,11 +467,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func updatePageLayout() { - guard let (size, insets, _) = self.containerLayout, let webPage = self.webPage else { + guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.webPage else { return } - let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) + let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) for (_, tileNode) in self.visibleTiles { tileNode.removeFromSupernode() @@ -920,7 +932,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } } - if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let page = content.instantPage, page.url == baseUrl || baseUrl.isEmpty, let anchor = anchor { + if let page = self.webPage?.instantPage, page.url == baseUrl || baseUrl.isEmpty, let anchor = anchor { self.scrollToAnchor(anchor) return } @@ -1029,7 +1041,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func openMedia(_ media: InstantPageMedia) { - guard let items = self.currentLayout?.items, let webPage = self.webPage else { + guard let items = self.currentLayout?.items, let (webPage, _) = self.webPage else { return } @@ -1157,7 +1169,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg let _ = saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in - if let self, let webPage = self.webPage, let image = media.media._asMedia() as? TelegramMediaImage { + if let self, let (webPage, _) = self.webPage, let image = media.media._asMedia() as? TelegramMediaImage { self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) } })], catchTapsOutside: true) @@ -1300,7 +1312,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in - if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .Loaded(content) = webPage.content { strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) } })] @@ -1368,7 +1380,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) { - guard let webPage = self.webPage else { + guard let (webPage, instantPage) = self.webPage else { return } @@ -1389,7 +1401,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg return } - let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in + let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, instantPage: instantPage, anchorText: anchorText, openUrl: { [weak self] url in self?.openUrl(url) }, openUrlIn: { [weak self] url in self?.openUrlIn(url) @@ -1444,7 +1456,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true) } - } else if case let .Loaded(content) = self.webPage?.content, let instantPage = content.instantPage, !instantPage.isComplete { + } else if let instantPage = self.webPage?.instantPage, !instantPage.isComplete { // self.loadProgress.set(0.5) self.pendingAnchor = anchor } @@ -1480,7 +1492,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } func addToRecentlyVisited() { - if let webPage = self.webPage { + if let (webPage, _) = self.webPage { let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone() } } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index c0a331c65e..77f0f87f52 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1019,6 +1019,11 @@ public final class ChatPresentationInterfaceState: Equatable { public func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { var inputQueryResults = self.inputQueryResults let updated = f(inputQueryResults[queryKind]) + if case .contextRequest = queryKind { + #if DEBUG + print("updatedInputQueryResult: \(String(describing: updated))") + #endif + } if let updated = updated { inputQueryResults[queryKind] = updated } else { diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index b18a26adce..3e989876b2 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -1284,7 +1284,7 @@ final class ComposePollScreenComponent: Component { component: AnyComponent(EmojiSuggestionsComponent( context: component.context, userLocation: .other, - theme: EmojiSuggestionsComponent.Theme(theme: environment.theme), + theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor), animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, files: value, diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 660a400c94..2f0b187bbf 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -64,10 +64,12 @@ private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, med return [] } -public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galleryMedia: Media) -> [InstantPageGalleryEntry] { +public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage.Accessor, galleryMedia: Media) -> [InstantPageGalleryEntry] { var result: [InstantPageGalleryEntry] = [] var counter: Int = 0 + let page = page._parse() + for block in page.blocks { result.append(contentsOf: instantPageBlockMedia(pageId: webpageId, block: block, media: page.media, counter: &counter)) } diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 8f2967487c..52108faac6 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -36,7 +36,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { private let pushController: (ViewController) -> Void private let openPeer: (EnginePeer) -> Void - private var webPage: TelegramMediaWebpage? + private var webPage: (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? private var initialAnchor: String? private var pendingAnchor: String? private var initialState: InstantPageStoredState? @@ -141,7 +141,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { self.navigationBar.back = navigateBack self.navigationBar.share = { [weak self] in - if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .Loaded(content) = webPage.content { let shareController = ShareController(context: context, subject: .url(content.url)) shareController.actionCompleted = { [weak self] in if let strongSelf = self { @@ -345,7 +345,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) { - if self.webPage != webPage { + if self.webPage?.webPage != webPage { if self.webPage != nil && self.currentLayout != nil { if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view) @@ -356,7 +356,15 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.setupScrollOffsetOnLayout = self.webPage == nil - self.webPage = webPage + if let webPage { + var instantPage: InstantPage? + if case let .Loaded(content) = webPage.content { + instantPage = content.instantPage?._parse() + } + self.webPage = (webPage, instantPage) + } else { + self.webPage = nil + } if let anchor = anchor { self.initialAnchor = anchor.removingPercentEncoding } else if let state = state { @@ -460,11 +468,11 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } private func updateLayout() { - guard let containerLayout = self.containerLayout, let webPage = self.webPage, let theme = self.theme else { + guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.webPage, let theme = self.theme else { return } - let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) + let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) for (_, tileNode) in self.visibleTiles { tileNode.removeFromSupernode() @@ -863,7 +871,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } var title: String? - if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + if let (webPage, _) = self.webPage, case let .Loaded(content) = webPage.content { title = content.websiteName } @@ -1027,7 +1035,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in - if let strongSelf = self, let webPage = strongSelf.webPage, case let .image(image) = media.media { + if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .image(image) = media.media { strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) } })], catchTapsOutside: true) @@ -1141,7 +1149,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in - if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .Loaded(content) = webPage.content { strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) } })] @@ -1209,7 +1217,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) { - guard let theme = self.theme, let webPage = self.webPage else { + guard let theme = self.theme, let (webPage, instantPage) = self.webPage else { return } @@ -1230,7 +1238,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { return } - let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in + let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, instantPage: instantPage, anchorText: anchorText, openUrl: { [weak self] url in self?.openUrl(url) }, openUrlIn: { [weak self] url in self?.openUrlIn(url) @@ -1285,7 +1293,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true) } - } else if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, !instantPage.isComplete { + } else if let (_, instantPage) = self.webPage, let instantPage, !instantPage.isComplete { self.loadProgress.set(0.5) self.pendingAnchor = anchor } @@ -1302,7 +1310,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { baseUrl = String(baseUrl[.. InstantPageLayout { +public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content } - guard let loadedContent = maybeLoadedContent, let instantPage = loadedContent.instantPage else { + guard let loadedContent = maybeLoadedContent, let instantPage else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift index 318ecae4ef..fbee7ce871 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift @@ -17,17 +17,17 @@ public final class InstantPageReferenceController: ViewController { private let context: AccountContext private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme - private let webPage: TelegramMediaWebpage + private let webPage: (webPage: TelegramMediaWebpage, instantPage: InstantPage?) private let anchorText: NSAttributedString private let openUrl: (InstantPageUrlItem) -> Void private let openUrlIn: (InstantPageUrlItem) -> Void private let present: (ViewController, Any?) -> Void - public init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + public init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, instantPage: InstantPage?, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.sourceLocation = sourceLocation self.theme = theme - self.webPage = webPage + self.webPage = (webPage, instantPage) self.anchorText = anchorText self.openUrl = openUrl self.openUrlIn = openUrlIn @@ -43,7 +43,7 @@ public final class InstantPageReferenceController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageReferenceControllerNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, anchorText: self.anchorText, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present) + self.displayNode = InstantPageReferenceControllerNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage.webPage, instantPage: self.webPage.instantPage, anchorText: self.anchorText, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index 887574d31a..550e8c2ef5 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -15,7 +15,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme private var presentationData: PresentationData - private let webPage: TelegramMediaWebpage + private let webPage: (webPage: TelegramMediaWebpage, instantPage: InstantPage?) private let anchorText: NSAttributedString private let dimNode: ASDisplayNode @@ -38,12 +38,12 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie var dismiss: (() -> Void)? var close: (() -> Void)? - init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, instantPage: InstantPage?, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.sourceLocation = sourceLocation self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = theme - self.webPage = webPage + self.webPage = (webPage, instantPage) self.anchorText = anchorText self.openUrl = openUrl self.openUrlIn = openUrlIn @@ -197,12 +197,12 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie self.contentNode?.removeFromSupernode() var media: [EngineMedia.Id: EngineMedia] = [:] - if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage { + if let instantPage = self.webPage.instantPage { media = instantPage.media.mapValues(EngineMedia.init) } let sideInset: CGFloat = 16.0 - let (_, items, contentSize) = layoutTextItemWithString(self.anchorText, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset), media: media, webpage: self.webPage) + let (_, items, contentSize) = layoutTextItemWithString(self.anchorText, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset), media: media, webpage: self.webPage.webPage) let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, sourceLocation: self.sourceLocation, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in }, getPreloadedResource: { url in return nil}) transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: width, height: contentSize.height))) self.contentContainerNode.insertSubnode(contentNode, at: 0) @@ -413,7 +413,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in - if let strongSelf = self, case let .Loaded(content) = strongSelf.webPage.content { + if let strongSelf = self, case let .Loaded(content) = strongSelf.webPage.webPage.content { strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) } })]) diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index daadeca6ed..6144b3c4f0 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -398,7 +398,7 @@ _captionMixin.stickersContext = stickersContext; [_captionMixin createInputPanelIfNeeded]; - _headerWrapperView = [[UIView alloc] init]; + _headerWrapperView = [[TGMediaPickerGalleryWrapperView alloc] init]; [_wrapperView addSubview:_headerWrapperView]; _photoCounterButton = [[TGMediaPickerPhotoCounterButton alloc] initWithFrame:CGRectMake(0, 0, 64, 38)]; @@ -1629,9 +1629,27 @@ #pragma mark - +- (UIView *)hitTestWithSpecialHandling:(UIView *)view point:(CGPoint)point withEvent:(UIEvent *)event { + for (UIView *subview in [view.subviews reverseObjectEnumerator]) { + if ([subview isKindOfClass:[TGMediaPickerGalleryWrapperView class]]) { + UIView *result = [self hitTestWithSpecialHandling:subview point:[view convertPoint:point toView:subview] withEvent:event]; + if (result) { + return result; + } + } else { + UIView *result = [subview hitTest:[view convertPoint:point toView:subview] withEvent:event]; + if (result) { + return result; + } + } + } + + return nil; +} + - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *view = [super hitTest:point withEvent:event]; + UIView *view = [self hitTestWithSpecialHandling:self point:point withEvent:event]; bool editingCover = false; if (_coverTitleLabel != nil && !_coverTitleLabel.isHidden) { diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift index 149bf8f97e..2026c7b8c7 100644 --- a/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift @@ -756,9 +756,14 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { self.pause() f() case let .loopDisablingSound(f): - self.stoppedAtEnd = false - self.isSoundEnabled = false - self.seek(timestamp: 0.0, play: true, notify: true) + if duration - 0.1 <= 0.0 { + self.stoppedAtEnd = true + self.pause() + } else { + self.stoppedAtEnd = false + self.isSoundEnabled = false + self.seek(timestamp: 0.0, play: true, notify: true) + } f() } } diff --git a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift index 4885747d7f..286ea150dc 100644 --- a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift +++ b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift @@ -14,7 +14,7 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal [TelegramMediaImageRepresentation] { var representations: [TelegramMediaImageRepresentation] = [] switch photo { diff --git a/submodules/TelegramCore/Sources/Network/FetchV2.swift b/submodules/TelegramCore/Sources/Network/FetchV2.swift index b7f52b98d3..073b7236a9 100644 --- a/submodules/TelegramCore/Sources/Network/FetchV2.swift +++ b/submodules/TelegramCore/Sources/Network/FetchV2.swift @@ -71,6 +71,10 @@ private final class FetchImpl { self.partRange = partRange self.fetchRange = fetchRange } + + deinit { + self.disposable?.dispose() + } } private final class PendingReadyPart { @@ -399,7 +403,7 @@ private final class FetchImpl { self.update() self.requiredRangesDisposable = (intervals - |> deliverOn(self.queue)).start(next: { [weak self] intervals in + |> deliverOn(self.queue)).startStrict(next: { [weak self] intervals in guard let `self` = self else { return } @@ -412,6 +416,7 @@ private final class FetchImpl { } deinit { + self.requiredRangesDisposable?.dispose() } private func update() { @@ -676,7 +681,7 @@ private final class FetchImpl { let cdnData = state.cdnData state.disposable = (reuploadSignal - |> deliverOn(self.queue)).start(next: { [weak self] result in + |> deliverOn(self.queue)).startStrict(next: { [weak self] result in guard let `self` = self else { return } @@ -713,7 +718,7 @@ private final class FetchImpl { info: info, resource: self.resource ) - |> deliverOn(self.queue)).start(next: { [weak self] validationResult in + |> deliverOn(self.queue)).startStrict(next: { [weak self] validationResult in guard let `self` = self else { return } @@ -875,7 +880,7 @@ private final class FetchImpl { if let filePartRequest { part.disposable = (filePartRequest - |> deliverOn(self.queue)).start(next: { [weak self, weak state, weak part] result in + |> deliverOn(self.queue)).startStrict(next: { [weak self, weak state, weak part] result in guard let self, let state, case let .fetching(fetchingState) = self.state, fetchingState === state else { return } @@ -969,7 +974,7 @@ private final class FetchImpl { let queue = self.queue hashRange.disposable = (fetchRequest - |> deliverOn(self.queue)).start(next: { [weak self, weak state, weak hashRange] result in + |> deliverOn(self.queue)).startStrict(next: { [weak self, weak state, weak hashRange] result in queue.async { guard let self, let state, case let .fetching(fetchingState) = self.state, fetchingState === state else { return diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index fbaabb7969..2cc69b8557 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -204,8 +204,8 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me return result } if let instantPage = content.instantPage { - for pageMedia in instantPage.media.values { - if let result = findMediaResource(media: pageMedia, previousMedia: previousMedia, resource: resource) { + for (_, pageMedia) in instantPage.media { + if let result = findMediaResource(media: pageMedia._parse(), previousMedia: previousMedia, resource: resource) { return result } } @@ -279,8 +279,8 @@ func findMediaResourceById(media: Media, resourceId: MediaResourceId) -> Telegra return result } if let instantPage = content.instantPage { - for pageMedia in instantPage.media.values { - if let result = findMediaResourceById(media: pageMedia, resourceId: resourceId) { + for (_, pageMedia) in instantPage.media { + if let result = findMediaResourceById(media: pageMedia._parse(), resourceId: resourceId) { return result } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift index 380ca70f05..8119febf8f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift @@ -1171,3 +1171,54 @@ public func TelegramMediaResource_serialize(resource: TelegramMediaResource, fla return nil } } + +public extension TelegramCore_TelegramMediaResource { + var id: MediaResourceId { + switch self.valueType { + case .telegrammediaresourceCloudfilemediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_CloudFileMediaResource.self) else { + return MediaResourceId("") + } + return MediaResourceId(CloudFileMediaResourceId(datacenterId: Int(value.datacenterId), volumeId: value.volumeId, localId: value.localId, secret: value.secret).uniqueId) + case .telegrammediaresourceClouddocumentsizemediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_CloudDocumentSizeMediaResource.self) else { + return MediaResourceId("") + } + return MediaResourceId(CloudDocumentSizeMediaResourceId(datacenterId: Int32(value.datacenterId), documentId: value.documentId, sizeSpec: value.sizeSpec).uniqueId) + case .telegrammediaresourceCloudphotosizemediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_CloudPhotoSizeMediaResource.self) else { + return MediaResourceId("") + } + return MediaResourceId(CloudPhotoSizeMediaResourceId(datacenterId: Int32(value.datacenterId), photoId: value.photoId, sizeSpec: value.sizeSpec).uniqueId) + case .telegrammediaresourceCloudpeerphotosizemediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_CloudPeerPhotoSizeMediaResource.self) else { + return MediaResourceId("") + } + let sizeSpec: CloudPeerPhotoSizeSpec + switch value.sizeSpec { + case .small: + sizeSpec = .small + case .fullSize: + sizeSpec = .fullSize + } + return MediaResourceId(CloudPeerPhotoSizeMediaResourceId(datacenterId: Int32(value.datacenterId), photoId: value.photoId, sizeSpec: sizeSpec, volumeId: value.volumeId, localId: value.localId).uniqueId) + case .telegrammediaresourceCloudstickerpackthumbnailmediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_CloudStickerPackThumbnailMediaResource.self) else { + return MediaResourceId("") + } + return MediaResourceId(CloudStickerPackThumbnailMediaResourceId(datacenterId: Int32(value.datacenterId), thumbVersion: value.thumbVersion, volumeId: value.volumeId, localId: value.localId).uniqueId) + case .telegrammediaresourceClouddocumentmediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_CloudDocumentMediaResource.self) else { + return MediaResourceId("") + } + return MediaResourceId(CloudDocumentMediaResourceId(datacenterId: Int(value.datacenterId), fileId: value.fileId).uniqueId) + case .telegrammediaresourceLocalfilemediaresource: + guard let value = self.value(type: TelegramCore_TelegramMediaResource_LocalFileMediaResource.self) else { + return MediaResourceId("") + } + return MediaResourceId(LocalFileMediaResourceId(fileId: value.fileId).uniqueId) + case .none_: + return MediaResourceId("") + } + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift index ebdea3244e..24288a45ba 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift @@ -1473,25 +1473,11 @@ public final class InstantPage: PostboxCoding, Equatable { return try InstantPageBlock(flatBuffersObject: flatBuffersObject.blocks(at: i)!) } - //TODO:release support other media types var media: [MediaId: Media] = [:] for i in 0 ..< flatBuffersObject.mediaCount { - let mediaItem = flatBuffersObject.media(at: i)! - switch mediaItem.valueType { - case .mediaTelegrammediafile: - guard let value = mediaItem.value(type: TelegramCore_Media_TelegramMediaFile.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) - } - let parsedMedia = try TelegramMediaFile(flatBuffersObject: value.file) - media[parsedMedia.fileId] = parsedMedia - case .mediaTelegrammediaimage: - guard let value = mediaItem.value(type: TelegramCore_Media_TelegramMediaImage.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) - } - let parsedMedia = try TelegramMediaImage(flatBuffersObject: value.image) - media[parsedMedia.imageId] = parsedMedia - case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + let parsedMedia = try TelegramMedia_parse(flatBuffersObject: flatBuffersObject.media(at: i)!) + if let id = parsedMedia.id { + media[id] = parsedMedia } } self.media = media @@ -1510,21 +1496,8 @@ public final class InstantPage: PostboxCoding, Equatable { var mediaOffsets: [Offset] = [] for (_, media) in self.media.sorted(by: { $0.key < $1.key }) { - switch media { - case let file as TelegramMediaFile: - let fileOffset = file.encodeToFlatBuffers(builder: &builder) - let start = TelegramCore_Media_TelegramMediaFile.startMedia_TelegramMediaFile(&builder) - TelegramCore_Media_TelegramMediaFile.add(file: fileOffset, &builder) - let offset = TelegramCore_Media_TelegramMediaFile.endMedia_TelegramMediaFile(&builder, start: start) - mediaOffsets.append(TelegramCore_Media.createMedia(&builder, valueType: .mediaTelegrammediafile, valueOffset: offset)) - case let image as TelegramMediaImage: - let imageOffset = image.encodeToFlatBuffers(builder: &builder) - let start = TelegramCore_Media_TelegramMediaImage.startMedia_TelegramMediaImage(&builder) - TelegramCore_Media_TelegramMediaImage.add(image: imageOffset, &builder) - let offset = TelegramCore_Media_TelegramMediaImage.endMedia_TelegramMediaImage(&builder, start: start) - mediaOffsets.append(TelegramCore_Media.createMedia(&builder, valueType: .mediaTelegrammediaimage, valueOffset: offset)) - default: - assertionFailure() + if let offset = TelegramMedia_serialize(media: media, flatBuffersBuilder: &builder) { + mediaOffsets.append(offset) } } @@ -1573,13 +1546,88 @@ public extension InstantPage { public static func ==(lhs: InstantPage.Accessor, rhs: InstantPage.Accessor) -> Bool { if let lhsWrappedInstantPage = lhs._wrappedInstantPage, let rhsWrappedInstantPage = rhs._wrappedInstantPage { - return lhsWrappedInstantPage === rhsWrappedInstantPage + return lhsWrappedInstantPage == rhsWrappedInstantPage } else if let lhsWrappedData = lhs._wrappedData, let rhsWrappedData = rhs._wrappedData { return lhsWrappedData == rhsWrappedData } else { - assertionFailure() return lhs._parse() == rhs._parse() } } } } + +public extension InstantPage.Accessor { + struct MediaIterator: Sequence, IteratorProtocol { + private let accessor: InstantPage.Accessor + private var wrappedInstantPageIterator: Dictionary.Iterator? + private var wrappedCurrentIndex: Int32 = 0 + + init(_ accessor: InstantPage.Accessor) { + self.accessor = accessor + + if let wrappedInstantPage = accessor._wrappedInstantPage { + self.wrappedInstantPageIterator = wrappedInstantPage.media.makeIterator() + } else { + self.wrappedInstantPageIterator = nil + } + } + + mutating public func next() -> (MediaId, TelegramMedia.Accessor)? { + if self.wrappedInstantPageIterator != nil { + guard let (id, value) = self.wrappedInstantPageIterator!.next() else { + return nil + } + return (id, TelegramMedia.Accessor(value)) + } + + if self.wrappedCurrentIndex >= self.accessor._wrapped!.mediaCount { + return nil + } + let index = self.wrappedCurrentIndex + self.wrappedCurrentIndex += 1 + let media = self.accessor._wrapped!.media(at: index)! + let parsedMedia = TelegramMedia.Accessor(media) + if let id = parsedMedia.id { + return (id, parsedMedia) + } else { + return nil + } + } + } + + var isComplete: Bool { + if let wrappedInstantPage = self._wrappedInstantPage { + return wrappedInstantPage.isComplete + } + + return self._wrapped!.isComplete + } + + var media: MediaIterator { + return MediaIterator(self) + } + + var views: Int32? { + if let wrappedInstantPage = self._wrappedInstantPage { + return wrappedInstantPage.views + } + + return self._wrapped!.views == Int32.min ? nil : self._wrapped!.views + } + + var url: String { + if let wrappedInstantPage = self._wrappedInstantPage { + return wrappedInstantPage.url + } + + return self._wrapped!.url + } + + var rtl: Bool { + if let wrappedInstantPage = self._wrappedInstantPage { + return wrappedInstantPage.rtl + } + + return self._wrapped!.rtl + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index 7437b40637..a6187637cb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -1,4 +1,6 @@ import Postbox +import FlatBuffers +import FlatSerialization private enum TelegramMediaWebpageAttributeTypes: Int32 { case unsupported @@ -177,7 +179,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let file: TelegramMediaFile? public let story: TelegramMediaStory? public let attributes: [TelegramMediaWebpageAttribute] - public let instantPage: InstantPage? + public let instantPage: InstantPage.Accessor? public init( url: String, @@ -218,7 +220,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.file = file self.story = story self.attributes = attributes - self.instantPage = instantPage + self.instantPage = instantPage.flatMap(InstantPage.Accessor.init) } public init(decoder: PostboxDecoder) { @@ -272,8 +274,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } self.attributes = effectiveAttributes - if let instantPage = decoder.decodeObjectForKey("ip", decoder: { InstantPage(decoder: $0) }) as? InstantPage { - self.instantPage = instantPage + if let serializedInstantPageData = decoder.decodeDataForKey("ipd") { + var byteBuffer = ByteBuffer(data: serializedInstantPageData) + self.instantPage = InstantPage.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_InstantPage, serializedInstantPageData) + } else if let instantPage = decoder.decodeObjectForKey("ip", decoder: { InstantPage(decoder: $0) }) as? InstantPage { + self.instantPage = InstantPage.Accessor(instantPage) } else { self.instantPage = nil } @@ -355,9 +360,19 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { encoder.encodeObjectArray(self.attributes, forKey: "attr") if let instantPage = self.instantPage { - encoder.encodeObject(instantPage, forKey: "ip") + if let instantPageData = instantPage._wrappedData { + encoder.encodeData(instantPageData, forKey: "ipd") + } else if let instantPage = instantPage._wrappedInstantPage { + var builder = FlatBufferBuilder(initialSize: 1024) + let value = instantPage.encodeToFlatBuffers(builder: &builder) + builder.finish(offset: value) + let serializedInstantPage = builder.data + encoder.encodeData(serializedInstantPage, forKey: "ipd") + } else { + preconditionFailure() + } } else { - encoder.encodeNil(forKey: "ip") + encoder.encodeNil(forKey: "ipd") } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 5242e9432f..21e053560e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -857,6 +857,38 @@ public extension TelegramEngine.EngineData.Item { } } + public struct PeerSettings: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.peerStatusSettings + } else if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.peerStatusSettings + } else if let cachedData = view.cachedPeerData as? CachedGroupData { + return cachedData.peerStatusSettings + } else { + return nil + } + } + } + public struct AreVideoCallsAvailable: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Bool diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift index 6e82c5dd51..646e3d6cc0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift @@ -169,6 +169,12 @@ public final class CachedPremiumGiftCodeOptions: Codable { } func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?, onlyCached: Bool = false) -> Signal<[PremiumGiftCodeOption], NoError> { + if let peerId { + if peerId.namespace == Namespaces.Peer.SecretChat { + return .single([]) + } + } + let cached = account.postbox.transaction { transaction -> Signal<[PremiumGiftCodeOption], NoError> in if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPremiumGiftCodeOptions, key: ValueBoxKey(length: 0)))?.get(CachedPremiumGiftCodeOptions.self) { return .single(entry.options) @@ -222,6 +228,12 @@ func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?, func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?) -> Signal<[PremiumGiftCodeOption], NoError> { + if let peerId { + if peerId.namespace == Namespaces.Peer.SecretChat { + return .single([]) + } + } + var flags: Int32 = 0 if let _ = peerId { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift b/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift new file mode 100644 index 0000000000..f87d067282 --- /dev/null +++ b/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift @@ -0,0 +1,92 @@ +import Foundation +import FlatBuffers +import FlatSerialization +import Postbox + +public func TelegramMedia_parse(flatBuffersObject: TelegramCore_Media) throws -> Media { + //TODO:release support other media types + switch flatBuffersObject.valueType { + case .mediaTelegrammediafile: + guard let value = flatBuffersObject.value(type: TelegramCore_Media_TelegramMediaFile.self) else { + throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + } + return try TelegramMediaFile(flatBuffersObject: value.file) + case .mediaTelegrammediaimage: + guard let value = flatBuffersObject.value(type: TelegramCore_Media_TelegramMediaImage.self) else { + throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + } + return try TelegramMediaImage(flatBuffersObject: value.image) + case .none_: + throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + } +} + +public func TelegramMedia_serialize(media: Media, flatBuffersBuilder builder: inout FlatBufferBuilder) -> Offset? { + //TODO:release support other media types + switch media { + case let file as TelegramMediaFile: + let fileOffset = file.encodeToFlatBuffers(builder: &builder) + let start = TelegramCore_Media_TelegramMediaFile.startMedia_TelegramMediaFile(&builder) + TelegramCore_Media_TelegramMediaFile.add(file: fileOffset, &builder) + let offset = TelegramCore_Media_TelegramMediaFile.endMedia_TelegramMediaFile(&builder, start: start) + return TelegramCore_Media.createMedia(&builder, valueType: .mediaTelegrammediafile, valueOffset: offset) + case let image as TelegramMediaImage: + let imageOffset = image.encodeToFlatBuffers(builder: &builder) + let start = TelegramCore_Media_TelegramMediaImage.startMedia_TelegramMediaImage(&builder) + TelegramCore_Media_TelegramMediaImage.add(image: imageOffset, &builder) + let offset = TelegramCore_Media_TelegramMediaImage.endMedia_TelegramMediaImage(&builder, start: start) + return TelegramCore_Media.createMedia(&builder, valueType: .mediaTelegrammediaimage, valueOffset: offset) + default: + assert(false) + return nil + } +} + +public enum TelegramMedia { + public struct Accessor { + let _wrappedMedia: Media? + let _wrapped: TelegramCore_Media? + + public init(_ wrapped: TelegramCore_Media) { + self._wrapped = wrapped + self._wrappedMedia = nil + } + + public init(_ wrapped: Media) { + self._wrapped = nil + self._wrappedMedia = wrapped + } + + public func _parse() -> Media { + if let _wrappedMedia = self._wrappedMedia { + return _wrappedMedia + } else { + return try! TelegramMedia_parse(flatBuffersObject: self._wrapped!) + } + } + } +} + +public extension TelegramMedia.Accessor { + var id: MediaId? { + //TODO:release support other media types + if let _wrappedMedia = self._wrappedMedia { + return _wrappedMedia.id + } + + switch self._wrapped!.valueType { + case .mediaTelegrammediafile: + guard let value = self._wrapped!.value(type: TelegramCore_Media_TelegramMediaFile.self) else { + return nil + } + return MediaId(value.file.fileId) + case .mediaTelegrammediaimage: + guard let value = self._wrapped!.value(type: TelegramCore_Media_TelegramMediaImage.self) else { + return nil + } + return MediaId(value.image.imageId) + case .none_: + return nil + } + } +} diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index e1b2e8114d..7109d300d1 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -253,7 +253,7 @@ public func actualizedWebpage(account: Account, webpage: TelegramMediaWebpage) - file: content.file, story: content.story, attributes: content.attributes, - instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }) + instantPage: (content.instantPage?._parse()).flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }) )) let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent) updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage) diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift index 38a60a4819..75a789bc82 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -103,6 +103,94 @@ public final class EmojiSuggestionsComponent: Component { } } + public static func searchData(context: AccountContext, isSavedMessages: Bool, query: String) -> Signal<[TelegramMediaFile], NoError> { + let hasPremium: Signal + if isSavedMessages { + hasPremium = .single(true) + } else { + hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + } + + if query.isSingleEmoji { + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium + ) + |> map { view, hasPremium -> [TelegramMediaFile] in + var result: [TelegramMediaFile] = [] + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem, !item.file.isPremiumEmoji || hasPremium else { + continue + } + let stringRepresentations = item.getStringRepresentationsOfIndexKeys() + for stringRepresentation in stringRepresentations { + if stringRepresentation == query { + result.append(item.file._parse()) + break + } + } + } + return result + } + } else { + let languageCode = "en-US" + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + return signal + |> mapToSignal { keywords -> Signal<[TelegramMediaFile], NoError> in + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium + ) + |> map { view, hasPremium -> [TelegramMediaFile] in + var result: [TelegramMediaFile] = [] + + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem, !item.file.isPremiumEmoji || hasPremium else { + continue + } + let stringRepresentations = item.getStringRepresentationsOfIndexKeys() + for stringRepresentation in stringRepresentations { + if let _ = allEmoticons[stringRepresentation] { + result.append(item.file._parse()) + break + } + } + } + + return result + } + } + } + } + public let context: AccountContext public let theme: Theme public let animationCache: AnimationCache @@ -389,8 +477,7 @@ public final class EmojiSuggestionsComponent: Component { let height: CGFloat = 54.0 if self.component?.theme.backgroundColor != component.theme.backgroundColor { - //self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor - self.backgroundLayer.fillColor = UIColor.black.cgColor + self.backgroundLayer.fillColor = component.theme.backgroundColor.cgColor self.blurView.updateColor(color: component.theme.backgroundColor, transition: .immediate) } var resetScrollingPosition = false @@ -435,8 +522,8 @@ public final class EmojiSuggestionsComponent: Component { } public extension EmojiSuggestionsComponent.Theme { - init(theme: PresentationTheme) { - self.backgroundColor = theme.list.plainBackgroundColor.withMultipliedAlpha(0.88) + init(theme: PresentationTheme, backgroundColor: UIColor? = nil) { + self.backgroundColor = backgroundColor ?? theme.list.plainBackgroundColor.withMultipliedAlpha(0.88) self.textColor = theme.list.itemPrimaryTextColor self.placeholderColor = theme.list.mediaPlaceholderColor } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 53c76bdf4f..670e715d85 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -198,11 +198,6 @@ public extension EmojiPagerContentComponent { searchCategories = .single(nil) } - #if DEBUG || true - var isFirstTime = true - let measure_startTime = CFAbsoluteTimeGetCurrent() - #endif - let emojiItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), forceHasPremium ? .single(true) : hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: premiumIfSavedMessages), @@ -214,54 +209,6 @@ public extension EmojiPagerContentComponent { ApplicationSpecificNotice.dismissedTrendingEmojiPacks(accountManager: context.sharedContext.accountManager) ) |> map { view, hasPremium, featuredEmojiPacks, availableReactions, searchCategories, iconStatusEmoji, peerSpecificPack, dismissedTrendingEmojiPacks -> EmojiPagerContentComponent in - #if DEBUG - if isFirstTime { - isFirstTime = false - - /*var files: [TelegramMediaFile] = [] - files.removeAll() - - if "".isEmpty { - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - files.append(item.file._parse()) - } - for featuredEmojiPack in featuredEmojiPacks { - for item in featuredEmojiPack.topItems { - files.append(item.file._parse()) - } - } - if let availableReactions { - for reactionItem in availableReactions.reactions { - files.append(reactionItem.staticIcon._parse()) - files.append(reactionItem.appearAnimation._parse()) - files.append(reactionItem.selectAnimation._parse()) - files.append(reactionItem.activateAnimation._parse()) - files.append(reactionItem.effectAnimation._parse()) - if let aroundAnimation = reactionItem.aroundAnimation { - files.append(aroundAnimation._parse()) - } - if let centerAnimation = reactionItem.centerAnimation { - files.append(centerAnimation._parse()) - } - } - } - Thread.current.threadDictionary["afwefw"] = files - } - - for file in files { - if file.fileId.id == 123 { - print("Interesting") - } - }*/ - - let measuredTime = CFAbsoluteTimeGetCurrent() - measure_startTime - print("emojiInputData init isMainThread: \(Thread.isMainThread): \(measuredTime * 1000.0) ms") - } - #endif - struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 0cdb29b57f..78b8b1a82b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -1395,7 +1395,7 @@ final class GiftSetupScreenComponent: Component { component: AnyComponent(EmojiSuggestionsComponent( context: component.context, userLocation: .other, - theme: EmojiSuggestionsComponent.Theme(theme: environment.theme), + theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor), animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, files: value, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 8fd7351d20..ad21bcfc7d 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -505,6 +505,7 @@ public final class MessageInputPanelComponent: Component { private var viewForOverlayContent: ViewForOverlayContent? private var currentEmojiSuggestionView: ComponentHostView? + private var currentEmojiSearchView: ComponentHostView? private var viewsIconView: UIImageView? private var viewStatsCountText: AnimatedCountLabelView? @@ -570,6 +571,7 @@ public final class MessageInputPanelComponent: Component { return } self.textFieldExternalState.dismissedEmojiSuggestionPosition = self.textFieldExternalState.currentEmojiSuggestion?.position + self.textFieldExternalState.dismissedEmojiSearchPosition = self.textFieldExternalState.currentEmojiSearch?.position self.state?.updated() } ) @@ -733,6 +735,15 @@ public final class MessageInputPanelComponent: Component { textFieldView.updateEmojiSuggestion(transition: .immediate) } self.state?.updated() + } else if let _ = self.textField.view, let currentEmojiSearch = self.textFieldExternalState.currentEmojiSearch, let currentEmojiSearchView = self.currentEmojiSearchView { + if let result = currentEmojiSearchView.hitTest(self.convert(point, to: currentEmojiSearchView), with: event) { + return result + } + self.textFieldExternalState.dismissedEmojiSearchPosition = currentEmojiSearch.position + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.updateEmojiSuggestion(transition: .immediate) + } + self.state?.updated() } if result == nil, let stickersResultPanel = self.stickersResultPanel?.view, let panelResult = stickersResultPanel.hitTest(self.convert(point, to: stickersResultPanel), with: event), panelResult !== stickersResultPanel { @@ -858,10 +869,10 @@ public final class MessageInputPanelComponent: Component { let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate let placeholderSize: CGSize if case let .plain(string) = component.placeholder, string.contains("#") { - let attributedPlaceholder = NSMutableAttributedString(string: string, font:Font.regular(17.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.3)) + let attributedPlaceholder = NSMutableAttributedString(string: string, font:Font.regular(17.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.4)) if let range = attributedPlaceholder.string.range(of: "#") { attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(component.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) - attributedPlaceholder.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff, alpha: 0.3), range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff, alpha: 0.4), range: NSRange(range, in: attributedPlaceholder.string)) attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) } @@ -905,7 +916,7 @@ public final class MessageInputPanelComponent: Component { transition: placeholderTransition, component: AnyComponent(AnimatedTextComponent( font: Font.regular(17.0), - color: UIColor(rgb: 0xffffff, alpha: 0.3), + color: UIColor(rgb: 0xffffff, alpha: 0.4), items: placeholderItems )), environment: {}, @@ -2179,6 +2190,18 @@ public final class MessageInputPanelComponent: Component { }) } + if let emojiSearch = self.textFieldExternalState.currentEmojiSearch, emojiSearch.disposable == nil { + emojiSearch.disposable = (EmojiSuggestionsComponent.searchData(context: component.context, isSavedMessages: false, query: emojiSearch.position.value) + |> deliverOnMainQueue).start(next: { [weak self, weak emojiSearch] result in + guard let self, let emojiSearch, self.textFieldExternalState.currentEmojiSearch === emojiSearch else { + return + } + + emojiSearch.value = result + self.state?.updated() + }) + } + var hasTrackingView = self.textFieldExternalState.hasTrackingView if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { hasTrackingView = false @@ -2201,6 +2224,20 @@ public final class MessageInputPanelComponent: Component { currentEmojiSuggestionView?.removeFromSuperview() }) } + + if let currentEmojiSearch = self.textFieldExternalState.currentEmojiSearch { + self.textFieldExternalState.currentEmojiSearch = nil + currentEmojiSearch.disposable?.dispose() + } + + if let currentEmojiSearchView = self.currentEmojiSearchView { + self.currentEmojiSearchView = nil + + currentEmojiSearchView.alpha = 0.0 + currentEmojiSearchView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSearchView] _ in + currentEmojiSearchView?.removeFromSuperview() + }) + } } if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile] { @@ -2213,8 +2250,6 @@ public final class MessageInputPanelComponent: Component { self.addSubview(currentEmojiSuggestionView) currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - - //self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView) } let globalPosition: CGPoint @@ -2232,7 +2267,7 @@ public final class MessageInputPanelComponent: Component { context: component.context, userLocation: .other, theme: EmojiSuggestionsComponent.Theme( - backgroundColor: UIColor(white: 0.0, alpha: 0.5), + backgroundColor: UIColor(white: 0.1, alpha: 1.0), textColor: .white, placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9) ), @@ -2251,7 +2286,7 @@ public final class MessageInputPanelComponent: Component { var text: String? var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? - loop: for attribute in file.attributes { + loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText @@ -2303,6 +2338,113 @@ public final class MessageInputPanelComponent: Component { } } + if let currentEmojiSearch = self.textFieldExternalState.currentEmojiSearch, let value = currentEmojiSearch.value as? [TelegramMediaFile], !value.isEmpty { + let currentEmojiSearchView: ComponentHostView + if let current = self.currentEmojiSearchView { + currentEmojiSearchView = current + } else { + currentEmojiSearchView = ComponentHostView() + self.currentEmojiSearchView = currentEmojiSearchView + self.addSubview(currentEmojiSearchView) + + currentEmojiSearchView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + var globalPosition: CGPoint + if let textView = self.textField.view { + globalPosition = textView.convert(currentEmojiSearch.localPosition, to: self) + globalPosition.x += 16.0 + } else { + globalPosition = .zero + } + + let sideInset: CGFloat = 7.0 + + let viewSize = currentEmojiSearchView.update( + transition: .immediate, + component: AnyComponent(EmojiSuggestionsComponent( + context: component.context, + userLocation: .other, + theme: EmojiSuggestionsComponent.Theme( + backgroundColor: UIColor(white: 0.1, alpha: 1.0), + textColor: .white, + placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9) + ), + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + files: value, + action: { [weak self] file in + guard let self, let textView = self.textField.view as? TextFieldComponent.View, let currentEmojiSearch = self.textFieldExternalState.currentEmojiSearch else { + return + } + + AudioServicesPlaySystemSound(0x450) + + let inputState = textView.getInputState() + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + var text: String? + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute = emojiAttribute, let text = text { + let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) + + var range = currentEmojiSearch.position.range + let previousText = inputText.attributedSubstring(from: range) + if range.location != 0 && inputText.attributedSubstring(from: NSRange(location: range.location - 1, length: range.length + 1)).string.hasPrefix(":") { + range = NSRange(location: range.location - 1, length: range.length + 1) + } + inputText.replaceCharacters(in: range, with: replacementText) + + var replacedUpperBound = range.lowerBound + while true { + if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { + let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) + if replaceRange.location < 0 { + break + } + let adjacentString = inputText.attributedSubstring(from: replaceRange) + if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { + break + } + inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) + replacedUpperBound = replaceRange.lowerBound + } else { + break + } + } + + let selectionPosition = range.lowerBound + (replacementText.string as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } + } + )), + environment: {}, + containerSize: CGSize(width: self.bounds.width - sideInset * 2.0, height: 100.0) + ) + + var viewFrame = CGRect(origin: CGPoint(x: globalPosition.x - floor((viewSize.width) * 0.5), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) + if viewFrame.origin.x + viewFrame.width > self.bounds.width - sideInset { + viewFrame.origin.x = self.bounds.width - sideInset - viewFrame.width + } + viewFrame.origin.x = max(viewFrame.origin.x, sideInset) + + currentEmojiSearchView.frame = viewFrame + if let componentView = currentEmojiSearchView.componentView as? EmojiSuggestionsComponent.View { + componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX)) + } + } + return size } } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 6f20c9acb7..8fa75ffef1 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -37,6 +37,9 @@ public final class TextFieldComponent: Component { public var currentEmojiSuggestion: EmojiSuggestion? public var dismissedEmojiSuggestionPosition: EmojiSuggestion.Position? + public var currentEmojiSearch: EmojiSearch? + public var dismissedEmojiSearchPosition: EmojiSearch.Position? + public init() { } } @@ -60,6 +63,25 @@ public final class TextFieldComponent: Component { } } + public final class EmojiSearch { + public struct Position: Equatable { + public var range: NSRange + public var value: String + } + + public var localPosition: CGPoint + public var position: Position + public var disposable: Disposable? + public var value: Any? + + init(localPosition: CGPoint, position: Position) { + self.localPosition = localPosition + self.position = position + self.disposable = nil + self.value = nil + } + } + public enum PasteData { case sticker(image: UIImage, isMemoji: Bool) case images([UIImage]) @@ -1248,6 +1270,51 @@ public final class TextFieldComponent: Component { } } } + } else { + if let index = selectedSubstring.string.range(of: ":", options: .backwards) { + let queryRange = index.upperBound ..< selectedSubstring.string.endIndex + let query = String(selectedSubstring.string[queryRange]) + if !query.isEmpty && !query.contains(where: { c in + for s in c.unicodeScalars { + if CharacterSet.whitespacesAndNewlines.contains(s) { + return true + } + } + return false + }) { + let beginning = self.textView.beginningOfDocument + let characterRange = NSRange(queryRange, in: selectedSubstring.string) + + let start = self.textView.position(from: beginning, offset: characterRange.location) + let end = self.textView.position(from: beginning, offset: characterRange.location + characterRange.length) + + if let start = start, let end = end, let textRange = self.textView.textRange(from: start, to: end) { + let selectionRects = self.textView.selectionRects(for: textRange) + let emojiSearchPosition = EmojiSearch.Position(range: characterRange, value: query) + + hasTracking = true + + if let trackingRect = selectionRects.first?.rect { + let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY) + if component.externalState.dismissedEmojiSearchPosition == emojiSearchPosition { + } else { + hasTrackingView = true + + let emojiSearch: EmojiSearch + if let current = component.externalState.currentEmojiSearch, current.position.value == emojiSearchPosition.value { + emojiSearch = current + } else { + emojiSearch = EmojiSearch(localPosition: trackingPosition, position: emojiSearchPosition) + component.externalState.currentEmojiSearch = emojiSearch + } + emojiSearch.localPosition = trackingPosition + emojiSearch.position = emojiSearchPosition + component.externalState.dismissedEmojiSearchPosition = nil + } + } + } + } + } } } if !hasTracking { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 823ea8c51c..06271060c2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -275,9 +275,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee |> castError(ChatContextQueryError.self) return signal |> then(commands) case let .contextRequest(addressName, query): - guard let peer else { + guard let chatPeerId = chatLocation.peerId else { return .single({ _ in return .contextRequestResult(nil, nil) }) } + var delayRequest = true var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() if let previousQuery = previousQuery { @@ -294,7 +295,6 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee signal = .single({ _ in return .contextRequestResult(nil, nil) }) } - let chatPeer = peer let contextBot = context.engine.peers.resolvePeerByName(name: addressName, referrer: nil) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { @@ -305,7 +305,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee |> castError(ChatContextQueryError.self) |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let contextResults = context.engine.messages.requestChatContextResults(botId: user.id, peerId: chatPeer.id, query: query, location: context.sharedContext.locationManager.flatMap { locationManager -> Signal<(Double, Double)?, NoError> in + let contextResults = context.engine.messages.requestChatContextResults(botId: user.id, peerId: chatPeerId, query: query, location: context.sharedContext.locationManager.flatMap { locationManager -> Signal<(Double, Double)?, NoError> in return `deferred` { Queue.mainQueue().async { requestBotLocationStatus(user.id) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 401d96e290..f077b8de82 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -3343,7 +3343,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch component: AnyComponent(EmojiSuggestionsComponent( context: context, userLocation: .other, - theme: EmojiSuggestionsComponent.Theme(theme: theme), + theme: EmojiSuggestionsComponent.Theme(theme: theme, backgroundColor: theme.list.itemBlocksBackgroundColor), animationCache: presentationContext.animationCache, animationRenderer: presentationContext.animationRenderer, files: value, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 5d247c8f57..95e775a17a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -189,6 +189,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return self.hasGroupCallOnScreenPromise.get() } + private var streamController: MediaStreamComponentController? + private var immediateHasOngoingCallValue = Atomic(value: false) public var immediateHasOngoingCall: Bool { return self.immediateHasOngoingCallValue.with { $0 } @@ -947,64 +949,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { } else if let current = self.currentCall, case .group = current { self.updateCurrentCall(call: nil) } - - /*if call.flatMap(VideoChatCall.group) != self.groupCallController?.call { - self.groupCallController?.dismiss(closing: true, manual: false) - self.groupCallController = nil - self.hasOngoingCall.set(false) - - if let call = call, let navigationController = mainWindow.viewController as? NavigationController { - mainWindow.hostView.containerView.endEditing(true) - - if call.isStream { - self.hasGroupCallOnScreenPromise.set(true) - let groupCallController = MediaStreamComponentController(call: call) - groupCallController.onViewDidAppear = { [weak self] in - if let self { - self.hasGroupCallOnScreenPromise.set(true) - } - } - groupCallController.onViewDidDisappear = { [weak self] in - if let self { - self.hasGroupCallOnScreenPromise.set(false) - } - } - groupCallController.navigationPresentation = .flatModal - groupCallController.parentNavigationController = navigationController - self.groupCallController = groupCallController - navigationController.pushViewController(groupCallController) - } else { - self.hasGroupCallOnScreenPromise.set(true) - - let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: call.accountContext, call: .group(call)) - |> deliverOnMainQueue).start(next: { [weak self, weak navigationController] initialData in - guard let self, let navigationController else { - return - } - - let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: call.accountContext, call: .group(call), initialData: initialData, sourceCallController: nil) - groupCallController.onViewDidAppear = { [weak self] in - if let self { - self.hasGroupCallOnScreenPromise.set(true) - } - } - groupCallController.onViewDidDisappear = { [weak self] in - if let self { - self.hasGroupCallOnScreenPromise.set(false) - } - } - groupCallController.navigationPresentation = .flatModal - groupCallController.parentNavigationController = navigationController - strongSelf.groupCallController = groupCallController - navigationController.pushViewController(groupCallController) - }) - } - - self.hasOngoingCall.set(true) - } else { - self.hasOngoingCall.set(false) - } - }*/ }) mainWindow.inCallNavigate = { [weak self] in @@ -1219,6 +1163,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.groupCallController = nil groupCallController.dismiss() } + if let streamController = self.streamController { + self.streamController = nil + streamController.dismiss() + } if shouldResetGroupCallOnScreen { self.hasGroupCallOnScreenPromise.set(.single(false)) @@ -1346,7 +1294,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } - if case let .group(groupCall) = call { + var groupCallIsStream = false + if case let .group(groupCall) = call, case let .group(value) = groupCall { + groupCallIsStream = value.isStream + } + + if case let .group(groupCall) = call, !groupCallIsStream { let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: groupCall.accountContext, call: groupCall) |> deliverOnMainQueue).start(next: { [weak self, weak transitioningToConferenceCallController] initialData in guard let self else { @@ -1399,6 +1352,17 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } + if case let .group(groupCall) = call, case let .group(group) = groupCall, groupCallIsStream { + if let navigationController = self.mainWindow?.viewController as? NavigationController { + let streamController = MediaStreamComponentController(call: group) + streamController.navigationPresentation = .flatModal + streamController.parentNavigationController = navigationController + self.streamController = streamController + + navigationController.pushViewController(streamController) + } + } + if self.currentCall != nil { self.callStateDisposable = (combineLatest(queue: .mainQueue(), self.hasGroupCallOnScreenPromise.get(), diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index 843dee34df..4b5b125320 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -15,7 +15,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/TgVoip:TgVoip", "//submodules/TgVoipWebrtc:TgVoipWebrtc", "//submodules/FFMpegBinding", "//submodules/ManagedFile", diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index e1863cd557..cf5002ad5f 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -4,7 +4,6 @@ import TelegramCore import Network import TelegramUIPreferences -import TgVoip import TgVoipWebrtc #if os(iOS) @@ -13,14 +12,6 @@ import AppBundle import Accelerate #endif -private func debugUseLegacyVersionForReflectors() -> Bool { - #if DEBUG && false - return true - #else - return false - #endif -} - private struct PeerTag: Hashable, CustomStringConvertible { var bytes: [UInt8] = Array(repeating: 0, count: 16) @@ -167,11 +158,6 @@ private func cleanupCallLogs(account: Account) { } private let setupLogs: Bool = { - OngoingCallThreadLocalContext.setupLoggingFunction({ value in - if let value = value { - Logger.shared.log("TGVOIP", value) - } - }) OngoingCallThreadLocalContextWebrtc.setupLoggingFunction({ value in if let value = value { Logger.shared.log("TGVOIP", value) @@ -253,26 +239,6 @@ private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCal } } -private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { - switch type { - case .none: - return .wifi - case .wifi: - return .wifi - case let .cellular(cellular): - switch cellular { - case .edge: - return .cellularEdge - case .gprs: - return .cellularGprs - case .thirdG, .unknown: - return .cellular3g - case .lte: - return .cellularLte - } - } -} - private func ongoingNetworkTypeForTypeWebrtc(_ type: NetworkType) -> OngoingCallNetworkTypeWebrtc { switch type { case .none: @@ -293,39 +259,6 @@ private func ongoingNetworkTypeForTypeWebrtc(_ type: NetworkType) -> OngoingCall } } -/*private func ongoingNetworkTypeForTypeWebrtcCustom(_ type: NetworkType) -> OngoingCallNetworkTypeWebrtcCustom { - switch type { - case .none: - return .wifi - case .wifi: - return .wifi - case let .cellular(cellular): - switch cellular { - case .edge: - return .cellularEdge - case .gprs: - return .cellularGprs - case .thirdG, .unknown: - return .cellular3g - case .lte: - return .cellularLte - } - } -}*/ - -private func ongoingDataSavingForType(_ type: VoiceCallDataSaving) -> OngoingCallDataSaving { - switch type { - case .never: - return .never - case .cellular: - return .cellular - case .always: - return .always - default: - return .never - } -} - private func ongoingDataSavingForTypeWebrtc(_ type: VoiceCallDataSaving) -> OngoingCallDataSavingWebrtc { switch type { case .never: @@ -363,56 +296,6 @@ private final class OngoingCallThreadLocalContextHolder { } } -extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { - func nativeSetNetworkType(_ type: NetworkType) { - self.setNetworkType(ongoingNetworkTypeForType(type)) - } - - func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) { - self.stop(completion) - } - - func nativeBeginTermination() { - } - - func nativeSetIsMuted(_ value: Bool) { - self.setIsMuted(value) - } - - func nativeSetIsLowBatteryLevel(_ value: Bool) { - } - - func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) { - } - - func nativeSetRequestedVideoAspect(_ aspect: Float) { - } - - func nativeDisableVideo() { - } - - func nativeSwitchVideoCamera() { - } - - func nativeDebugInfo() -> String { - return self.debugInfo() ?? "" - } - - func nativeVersion() -> String { - return self.version() ?? "" - } - - func nativeGetDerivedState() -> Data { - return self.getDerivedState() - } - - func addExternalAudioData(data: Data) { - } - - func nativeSetIsAudioSessionActive(isActive: Bool) { - } -} - #if targetEnvironment(simulator) private extension UIImage { @available(iOS 13.0, *) @@ -816,23 +699,6 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt } } -private extension OngoingCallContextState.State { - init(_ state: OngoingCallState) { - switch state { - case .initializing: - self = .initializing - case .connected: - self = .connected - case .failed: - self = .failed - case .reconnecting: - self = .reconnecting - default: - self = .failed - } - } -} - private extension OngoingCallContextState.State { init(_ state: OngoingCallStateWebrtc) { switch state { @@ -1014,7 +880,7 @@ public final class OngoingCallContext { private var networkTypeDisposable: Disposable? public static var maxLayer: Int32 { - return OngoingCallThreadLocalContext.maxLayer() + return OngoingCallThreadLocalContextWebrtc.maxLayer() } private let tempStatsLogFile: EngineTempBox.File @@ -1024,26 +890,15 @@ public final class OngoingCallContext { private let audioDevice: AudioDevice? public static func versions(includeExperimental: Bool, includeReference: Bool) -> [(version: String, supportsVideo: Bool)] { - #if os(iOS) && DEBUG && false - if "".isEmpty { - return [("5.0.0", true)] - } - #endif - - if debugUseLegacyVersionForReflectors() { - return [(OngoingCallThreadLocalContext.version(), true)] - } else { - var result: [(version: String, supportsVideo: Bool)] = [(OngoingCallThreadLocalContext.version(), false)] - result.append(contentsOf: OngoingCallThreadLocalContextWebrtc.versions(withIncludeReference: includeReference).map { version -> (version: String, supportsVideo: Bool) in - return (version, true) - }) - return result - } + var result: [(version: String, supportsVideo: Bool)] = [] + result.append(contentsOf: OngoingCallThreadLocalContextWebrtc.versions(withIncludeReference: includeReference).map { version -> (version: String, supportsVideo: Bool) in + return (version, true) + }) + return result } public init(account: Account, callSessionManager: CallSessionManager, callId: CallId, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal, serializedData: String?, dataSaving: VoiceCallDataSaving, key: Data, isOutgoing: Bool, video: OngoingCallVideoCapturer?, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowP2P: Bool, enableTCP: Bool, enableStunMarking: Bool, audioSessionActive: Signal, logName: String, preferredVideoCodec: String?, audioDevice: AudioDevice?) { let _ = setupLogs - OngoingCallThreadLocalContext.applyServerConfig(serializedData) self.callId = callId self.internalId = internalId @@ -1068,311 +923,274 @@ public final class OngoingCallContext { |> take(1) |> deliverOn(queue)).start(next: { [weak self] _ in if let strongSelf = self { - var useModernImplementation = true - var version = version var allowP2P = allowP2P - if debugUseLegacyVersionForReflectors() { - useModernImplementation = true - version = "12.0.0" - allowP2P = false - } else { - useModernImplementation = version != OngoingCallThreadLocalContext.version() + + var voipProxyServer: VoipProxyServerWebrtc? + if let proxyServer = proxyServer { + switch proxyServer.connection { + case let .socks5(username, password): + voipProxyServer = VoipProxyServerWebrtc(host: proxyServer.host, port: proxyServer.port, username: username, password: password) + case .mtp: + break + } } - if useModernImplementation { - var voipProxyServer: VoipProxyServerWebrtc? - if let proxyServer = proxyServer { - switch proxyServer.connection { - case let .socks5(username, password): - voipProxyServer = VoipProxyServerWebrtc(host: proxyServer.host, port: proxyServer.port, username: username, password: password) - case .mtp: - break - } - } - - var unfilteredConnections: [CallSessionConnection] - unfilteredConnections = [connections.primary] + connections.alternatives - - if version == "12.0.0" { - for connection in unfilteredConnections { - if case let .reflector(reflector) = connection { - unfilteredConnections.append(.reflector(CallSessionConnection.Reflector( - id: 123456, - ip: "91.108.9.38", - ipv6: "", - isTcp: true, - port: 595, - peerTag: reflector.peerTag - ))) - } - } - } - - var reflectorIdList: [Int64] = [] + var unfilteredConnections: [CallSessionConnection] + unfilteredConnections = [connections.primary] + connections.alternatives + + if version == "12.0.0" { for connection in unfilteredConnections { - switch connection { - case let .reflector(reflector): - reflectorIdList.append(reflector.id) - case .webRtcReflector: - break + if case let .reflector(reflector) = connection { + unfilteredConnections.append(.reflector(CallSessionConnection.Reflector( + id: 123456, + ip: "91.108.9.38", + ipv6: "", + isTcp: true, + port: 595, + peerTag: reflector.peerTag + ))) } } - - reflectorIdList.sort() - - var reflectorIdMapping: [Int64: UInt8] = [:] - for i in 0 ..< reflectorIdList.count { - reflectorIdMapping[reflectorIdList[i]] = UInt8(i + 1) + } + + var reflectorIdList: [Int64] = [] + for connection in unfilteredConnections { + switch connection { + case let .reflector(reflector): + reflectorIdList.append(reflector.id) + case .webRtcReflector: + break } + } + + reflectorIdList.sort() + + var reflectorIdMapping: [Int64: UInt8] = [:] + for i in 0 ..< reflectorIdList.count { + reflectorIdMapping[reflectorIdList[i]] = UInt8(i + 1) + } + + var signalingReflector: OngoingCallConnectionDescriptionWebrtc? + + var processedConnections: [CallSessionConnection] = [] + var filteredConnections: [OngoingCallConnectionDescriptionWebrtc] = [] + connectionsLoop: for connection in unfilteredConnections { + if processedConnections.contains(connection) { + continue + } + processedConnections.append(connection) - var signalingReflector: OngoingCallConnectionDescriptionWebrtc? - - var processedConnections: [CallSessionConnection] = [] - var filteredConnections: [OngoingCallConnectionDescriptionWebrtc] = [] - connectionsLoop: for connection in unfilteredConnections { - if processedConnections.contains(connection) { - continue - } - processedConnections.append(connection) - - switch connection { - case let .reflector(reflector): - if reflector.isTcp { - if version == "12.0.0" { - /*if signalingReflector == nil { - signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag)) - }*/ - } else { - if signalingReflector == nil { - signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag)) - } - - continue connectionsLoop - } - } - case .webRtcReflector: - break - } - - var webrtcConnections: [OngoingCallConnectionDescriptionWebrtc] = [] - for connection in callConnectionDescriptionsWebrtc(connection, idMapping: reflectorIdMapping) { - webrtcConnections.append(connection) - } - - filteredConnections.append(contentsOf: webrtcConnections) - } - - if let signalingReflector = signalingReflector { - if #available(iOS 12.0, *) { - let peerTag = dataWithHexString(signalingReflector.password) - - strongSelf.signalingConnectionManager = QueueLocalObject(queue: queue, generate: { - return CallSignalingConnectionManager(queue: queue, peerTag: peerTag, servers: [signalingReflector], dataReceived: { data in - guard let strongSelf = self else { - return - } - strongSelf.withContext { context in - if let context = context as? OngoingCallThreadLocalContextWebrtc { - context.addSignaling(data) - } - } - }) - }) - } - } - - var directConnection: OngoingCallDirectConnection? - if version == "9.0.0" && !"".isEmpty { - if #available(iOS 12.0, *) { - for connection in filteredConnections { - if connection.username == "reflector" && connection.reflectorId == 1 && !connection.hasTcp && connection.hasTurn { - directConnection = CallDirectConnectionImpl(host: connection.ip, port: Int(connection.port), peerTag: dataWithHexString(connection.password)) - break + switch connection { + case let .reflector(reflector): + if reflector.isTcp { + if version == "12.0.0" { + /*if signalingReflector == nil { + signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag)) + }*/ + } else { + if signalingReflector == nil { + signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag)) } + + continue connectionsLoop } } - } else { - directConnection = nil + case .webRtcReflector: + break } - #if DEBUG && true - var customParameters = customParameters - if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] { - var customParametersValue: [String: Any] - customParametersValue = initialCustomParameters - if version == "12.0.0" { - customParametersValue["network_use_tcponly"] = true as NSNumber - customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)! - } + var webrtcConnections: [OngoingCallConnectionDescriptionWebrtc] = [] + for connection in callConnectionDescriptionsWebrtc(connection, idMapping: reflectorIdMapping) { + webrtcConnections.append(connection) + } + + filteredConnections.append(contentsOf: webrtcConnections) + } + + if let signalingReflector = signalingReflector { + if #available(iOS 12.0, *) { + let peerTag = dataWithHexString(signalingReflector.password) - if let value = customParametersValue["network_use_tcponly"] as? Bool, value { - filteredConnections = filteredConnections.filter { connection in - if connection.hasTcp { - return true - } - return false - } - allowP2P = false - } - } - #endif - - /*#if DEBUG - if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] { - var customParametersValue: [String: Any] - customParametersValue = initialCustomParameters - customParametersValue["network_kcp_experiment"] = true as NSNumber - customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)! - } - #endif*/ - - let context = OngoingCallThreadLocalContextWebrtc( - version: version, - customParameters: customParameters, - queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), - proxy: voipProxyServer, - networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), - dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), - derivedState: Data(), - key: key, - isOutgoing: isOutgoing, - connections: filteredConnections, - maxLayer: maxLayer, - allowP2P: allowP2P, - allowTCP: enableTCP, - enableStunMarking: enableStunMarking, - logPath: logPath, - statsLogPath: tempStatsLogPath, - sendSignalingData: { [weak callSessionManager] data in - queue.async { + strongSelf.signalingConnectionManager = QueueLocalObject(queue: queue, generate: { + return CallSignalingConnectionManager(queue: queue, peerTag: peerTag, servers: [signalingReflector], dataReceived: { data in guard let strongSelf = self else { return } - if let signalingConnectionManager = strongSelf.signalingConnectionManager { - signalingConnectionManager.with { impl in - impl.send(payloadData: data) + strongSelf.withContext { context in + if let context = context as? OngoingCallThreadLocalContextWebrtc { + context.addSignaling(data) } } - - if let callSessionManager = callSessionManager { - callSessionManager.sendSignalingData(internalId: internalId, data: data) - } + }) + }) + } + } + + var directConnection: OngoingCallDirectConnection? + if version == "9.0.0" && !"".isEmpty { + if #available(iOS 12.0, *) { + for connection in filteredConnections { + if connection.username == "reflector" && connection.reflectorId == 1 && !connection.hasTcp && connection.hasTurn { + directConnection = CallDirectConnectionImpl(host: connection.ip, port: Int(connection.port), peerTag: dataWithHexString(connection.password)) + break } - }, - videoCapturer: video?.impl, - preferredVideoCodec: preferredVideoCodec, - audioInputDeviceId: "", - audioDevice: audioDevice?.impl, - directConnection: directConnection - ) + } + } + } else { + directConnection = nil + } + + #if DEBUG && true + var customParameters = customParameters + if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] { + var customParametersValue: [String: Any] + customParametersValue = initialCustomParameters + if version == "12.0.0" { + customParametersValue["network_use_tcponly"] = true as NSNumber + customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)! + } - strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) - context.stateChanged = { [weak callSessionManager] state, videoState, remoteVideoState, remoteAudioState, remoteBatteryLevel, _ in + if let value = customParametersValue["network_use_tcponly"] as? Bool, value { + filteredConnections = filteredConnections.filter { connection in + if connection.hasTcp { + return true + } + return false + } + allowP2P = false + } + } + #endif + + /*#if DEBUG + if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] { + var customParametersValue: [String: Any] + customParametersValue = initialCustomParameters + customParametersValue["network_kcp_experiment"] = true as NSNumber + customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)! + } + #endif*/ + + let context = OngoingCallThreadLocalContextWebrtc( + version: version, + customParameters: customParameters, + queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), + proxy: voipProxyServer, + networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), + dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), + derivedState: Data(), + key: key, + isOutgoing: isOutgoing, + connections: filteredConnections, + maxLayer: maxLayer, + allowP2P: allowP2P, + allowTCP: enableTCP, + enableStunMarking: enableStunMarking, + logPath: logPath, + statsLogPath: tempStatsLogPath, + sendSignalingData: { [weak callSessionManager] data in queue.async { guard let strongSelf = self else { return } - let mappedState = OngoingCallContextState.State(state) - let mappedVideoState: OngoingCallContextState.VideoState - switch videoState { - case .inactive: - mappedVideoState = .inactive - case .active: - mappedVideoState = .active - case .paused: - mappedVideoState = .paused - @unknown default: - mappedVideoState = .notAvailable + if let signalingConnectionManager = strongSelf.signalingConnectionManager { + signalingConnectionManager.with { impl in + impl.send(payloadData: data) + } } - let mappedRemoteVideoState: OngoingCallContextState.RemoteVideoState - switch remoteVideoState { - case .inactive: - mappedRemoteVideoState = .inactive - case .active: - mappedRemoteVideoState = .active - case .paused: - mappedRemoteVideoState = .paused - @unknown default: - mappedRemoteVideoState = .inactive + + if let callSessionManager = callSessionManager { + callSessionManager.sendSignalingData(internalId: internalId, data: data) } - let mappedRemoteAudioState: OngoingCallContextState.RemoteAudioState - switch remoteAudioState { - case .active: - mappedRemoteAudioState = .active - case .muted: - mappedRemoteAudioState = .muted - @unknown default: - mappedRemoteAudioState = .active - } - let mappedRemoteBatteryLevel: OngoingCallContextState.RemoteBatteryLevel - switch remoteBatteryLevel { - case .normal: - mappedRemoteBatteryLevel = .normal - case .low: - mappedRemoteBatteryLevel = .low - @unknown default: - mappedRemoteBatteryLevel = .normal - } - if case .active = mappedVideoState, !strongSelf.didReportCallAsVideo { - strongSelf.didReportCallAsVideo = true - callSessionManager?.updateCallType(internalId: internalId, type: .video) - } - strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel))) } - } - strongSelf.receptionPromise.set(.single(4)) - context.signalBarsChanged = { signalBars in - self?.receptionPromise.set(.single(signalBars)) - } - context.audioLevelUpdated = { level in - self?.audioLevelPromise.set(.single(level)) - } - - if audioDevice == nil { - strongSelf.audioSessionActiveDisposable.set((audioSessionActive - |> deliverOn(queue)).start(next: { isActive in - guard let strongSelf = self else { - return - } - strongSelf.withContext { context in - context.nativeSetIsAudioSessionActive(isActive: isActive) - } - })) - } - - strongSelf.networkTypeDisposable = (updatedNetworkType - |> deliverOn(queue)).start(next: { networkType in - self?.withContext { context in - context.nativeSetNetworkType(networkType) + }, + videoCapturer: video?.impl, + preferredVideoCodec: preferredVideoCodec, + audioInputDeviceId: "", + audioDevice: audioDevice?.impl, + directConnection: directConnection + ) + + strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) + context.stateChanged = { [weak callSessionManager] state, videoState, remoteVideoState, remoteAudioState, remoteBatteryLevel, _ in + queue.async { + guard let strongSelf = self else { + return } - }) - } else { - var voipProxyServer: VoipProxyServer? - if let proxyServer = proxyServer { - switch proxyServer.connection { - case let .socks5(username, password): - voipProxyServer = VoipProxyServer(host: proxyServer.host, port: proxyServer.port, username: username, password: password) - case .mtp: - break + let mappedState = OngoingCallContextState.State(state) + let mappedVideoState: OngoingCallContextState.VideoState + switch videoState { + case .inactive: + mappedVideoState = .inactive + case .active: + mappedVideoState = .active + case .paused: + mappedVideoState = .paused + @unknown default: + mappedVideoState = .notAvailable } - } - let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType), dataSaving: ongoingDataSavingForType(dataSaving), derivedState: Data(), key: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescription(connections.primary)!, alternativeConnections: connections.alternatives.compactMap(callConnectionDescription), maxLayer: maxLayer, allowP2P: allowP2P, logPath: logPath) - - strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) - context.stateChanged = { state in - self?.contextState.set(.single(OngoingCallContextState(state: OngoingCallContextState.State(state), videoState: .notAvailable, remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal))) - } - context.signalBarsChanged = { signalBars in - self?.receptionPromise.set(.single(signalBars)) - } - - strongSelf.networkTypeDisposable = (updatedNetworkType - |> deliverOn(queue)).start(next: { networkType in - self?.withContext { context in - context.nativeSetNetworkType(networkType) + let mappedRemoteVideoState: OngoingCallContextState.RemoteVideoState + switch remoteVideoState { + case .inactive: + mappedRemoteVideoState = .inactive + case .active: + mappedRemoteVideoState = .active + case .paused: + mappedRemoteVideoState = .paused + @unknown default: + mappedRemoteVideoState = .inactive } - }) + let mappedRemoteAudioState: OngoingCallContextState.RemoteAudioState + switch remoteAudioState { + case .active: + mappedRemoteAudioState = .active + case .muted: + mappedRemoteAudioState = .muted + @unknown default: + mappedRemoteAudioState = .active + } + let mappedRemoteBatteryLevel: OngoingCallContextState.RemoteBatteryLevel + switch remoteBatteryLevel { + case .normal: + mappedRemoteBatteryLevel = .normal + case .low: + mappedRemoteBatteryLevel = .low + @unknown default: + mappedRemoteBatteryLevel = .normal + } + if case .active = mappedVideoState, !strongSelf.didReportCallAsVideo { + strongSelf.didReportCallAsVideo = true + callSessionManager?.updateCallType(internalId: internalId, type: .video) + } + strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel))) + } } + strongSelf.receptionPromise.set(.single(4)) + context.signalBarsChanged = { signalBars in + self?.receptionPromise.set(.single(signalBars)) + } + context.audioLevelUpdated = { level in + self?.audioLevelPromise.set(.single(level)) + } + + if audioDevice == nil { + strongSelf.audioSessionActiveDisposable.set((audioSessionActive + |> deliverOn(queue)).start(next: { isActive in + guard let strongSelf = self else { + return + } + strongSelf.withContext { context in + context.nativeSetIsAudioSessionActive(isActive: isActive) + } + })) + } + + strongSelf.networkTypeDisposable = (updatedNetworkType + |> deliverOn(queue)).start(next: { networkType in + self?.withContext { context in + context.nativeSetNetworkType(networkType) + } + }) strongSelf.signalingDataDisposable = callSessionManager.beginReceivingCallSignalingData(internalId: internalId, { [weak self] dataList in queue.async { diff --git a/submodules/TgVoip/BUILD b/submodules/TgVoip/BUILD deleted file mode 100644 index 6ddbb134f0..0000000000 --- a/submodules/TgVoip/BUILD +++ /dev/null @@ -1,82 +0,0 @@ - -copts_arm = [ - "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DWEBRTC_APM_DEBUG_DUMP=0", - "-DWEBRTC_POSIX", - "-DTGVOIP_HAVE_TGLOG", - "-DWEBRTC_NS_FLOAT", - "-DWEBRTC_IOS", - "-DWEBRTC_HAS_NEON", - "-DTGVOIP_NO_DSP", -] - -copts_x86 = [ - "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DWEBRTC_APM_DEBUG_DUMP=0", - "-DWEBRTC_POSIX", - "-DTGVOIP_HAVE_TGLOG", - "-DTGVOIP_NO_DSP", - "-DWEBRTC_NS_FLOAT", - "-DWEBRTC_IOS", -] - -objc_library( - name = "TgVoip", - enable_modules = True, - module_name = "TgVoip", - srcs = glob([ - "Sources/**/*.m", - "Sources/**/*.mm", - "Sources/**/*.h", - "libtgvoip/*.h", - "libtgvoip/*.hpp", - "libtgvoip/*.m", - "libtgvoip/*.mm", - "libtgvoip/*.cpp", - "libtgvoip/audio/*.h", - "libtgvoip/audio/*.cpp", - "libtgvoip/video/*.h", - "libtgvoip/video/*.cpp", - "libtgvoip/os/darwin/*.h", - "libtgvoip/os/darwin/*.m", - "libtgvoip/os/darwin/*.mm", - "libtgvoip/os/darwin/*.cpp", - "libtgvoip/os/posix/*.h", - "libtgvoip/os/posix/*.cpp", - ], exclude = ["libtgvoip/os/darwin/*OSX*"]), - hdrs = glob([ - "PublicHeaders/**/*.h", - ]), - copts = [ - "-I{}/PublicHeaders/TgVoip".format(package_name()), - "-I{}/libtgvoip".format(package_name()), - "-I{}/third-party/webrtc/webrtc".format(package_name()), - "-Isubmodules/Opus/Public/opus", - "-DTGVOIP_USE_INSTALLED_OPUS", - "-Drtc=rtc1", - "-Dwebrtc=webrtc1", - ] + select({ - "@build_bazel_rules_apple//apple:ios_arm64": copts_arm, - "//build-system:ios_sim_arm64": copts_arm, - "@build_bazel_rules_apple//apple:ios_x86_64": copts_x86, - }), - includes = [ - "PublicHeaders", - ], - deps = [ - "//submodules/MtProtoKit:MtProtoKit", - "//third-party/opus:opus", - ], - sdk_frameworks = [ - "Foundation", - "UIKit", - "AudioToolbox", - "VideoToolbox", - "CoreTelephony", - "CoreMedia", - "AVFoundation", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/TgVoip/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h b/submodules/TgVoip/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h deleted file mode 100644 index d7f6e16846..0000000000 --- a/submodules/TgVoip/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef OngoingCallContext_h -#define OngoingCallContext_h - -#import - -@interface OngoingCallConnectionDescription : NSObject - -@property (nonatomic, readonly) int64_t connectionId; -@property (nonatomic, strong, readonly) NSString * _Nonnull ip; -@property (nonatomic, strong, readonly) NSString * _Nonnull ipv6; -@property (nonatomic, readonly) int32_t port; -@property (nonatomic, strong, readonly) NSData * _Nonnull peerTag; - -- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag; - -@end - -typedef NS_ENUM(int32_t, OngoingCallState) { - OngoingCallStateInitializing, - OngoingCallStateConnected, - OngoingCallStateFailed, - OngoingCallStateReconnecting -}; - -typedef NS_ENUM(int32_t, OngoingCallNetworkType) { - OngoingCallNetworkTypeWifi, - OngoingCallNetworkTypeCellularGprs, - OngoingCallNetworkTypeCellularEdge, - OngoingCallNetworkTypeCellular3g, - OngoingCallNetworkTypeCellularLte -}; - -typedef NS_ENUM(int32_t, OngoingCallDataSaving) { - OngoingCallDataSavingNever, - OngoingCallDataSavingCellular, - OngoingCallDataSavingAlways -}; - -@protocol OngoingCallThreadLocalContextQueue - -- (void)dispatch:(void (^ _Nonnull)())f; -- (bool)isCurrent; - -@end - -@interface VoipProxyServer : NSObject - -@property (nonatomic, strong, readonly) NSString * _Nonnull host; -@property (nonatomic, readonly) int32_t port; -@property (nonatomic, strong, readonly) NSString * _Nullable username; -@property (nonatomic, strong, readonly) NSString * _Nullable password; - -- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password; - -@end - -@interface OngoingCallThreadLocalContext : NSObject - -+ (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction; -+ (void)applyServerConfig:(NSString * _Nullable)data; -+ (int32_t)maxLayer; -+ (NSString * _Nonnull)version; - -@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); -@property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t); - -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType dataSaving:(OngoingCallDataSaving)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath; -- (void)stop:(void (^_Nonnull)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile))completion; - -- (bool)needRate; - -- (NSString * _Nullable)debugInfo; -- (NSString * _Nullable)version; -- (NSData * _Nonnull)getDerivedState; - -- (void)setIsMuted:(bool)isMuted; -- (void)setNetworkType:(OngoingCallNetworkType)networkType; - -@end - -#endif diff --git a/submodules/TgVoip/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoip/Sources/OngoingCallThreadLocalContext.mm deleted file mode 100644 index e650e4dec9..0000000000 --- a/submodules/TgVoip/Sources/OngoingCallThreadLocalContext.mm +++ /dev/null @@ -1,419 +0,0 @@ -#import "OngoingCallThreadLocalContext.h" - -#import "TgVoip.h" - -#import -#include - -#import - -static void TGCallAesIgeEncrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { - MTAesEncryptRaw(inBytes, outBytes, length, key, iv); -} - -static void TGCallAesIgeDecrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { - MTAesDecryptRaw(inBytes, outBytes, length, key, iv); -} - -static void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output) { - MTRawSha1(msg, length, output); -} - -static void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) { - MTRawSha256(msg, length, output); -} - -static void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num) { - uint8_t *outData = (uint8_t *)malloc(length); - MTAesCtr *aesCtr = [[MTAesCtr alloc] initWithKey:key keyLength:32 iv:iv ecount:ecount num:*num]; - [aesCtr encryptIn:inOut out:outData len:length]; - memcpy(inOut, outData, length); - free(outData); - - [aesCtr getIv:iv]; - - memcpy(ecount, [aesCtr ecount], 16); - *num = [aesCtr num]; -} - -static void TGCallRandomBytes(uint8_t *buffer, size_t length) { - arc4random_buf(buffer, length); -} - -@implementation OngoingCallConnectionDescription - -- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag { - self = [super init]; - if (self != nil) { - _connectionId = connectionId; - _ip = ip; - _ipv6 = ipv6; - _port = port; - _peerTag = peerTag; - } - return self; -} - -@end - -static MTAtomic *callContexts() { - static MTAtomic *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[MTAtomic alloc] initWithValue:[[NSMutableDictionary alloc] init]]; - }); - return instance; -} - -@interface OngoingCallThreadLocalContextReference : NSObject - -@property (nonatomic, weak) OngoingCallThreadLocalContext *context; -@property (nonatomic, strong, readonly) id queue; - -@end - -@implementation OngoingCallThreadLocalContextReference - -- (instancetype)initWithContext:(OngoingCallThreadLocalContext *)context queue:(id)queue { - self = [super init]; - if (self != nil) { - self.context = context; - _queue = queue; - } - return self; -} - -@end - -static int32_t nextId = 1; - -static int32_t addContext(OngoingCallThreadLocalContext *context, id queue) { - int32_t contextId = OSAtomicIncrement32(&nextId); - [callContexts() with:^id(NSMutableDictionary *dict) { - dict[@(contextId)] = [[OngoingCallThreadLocalContextReference alloc] initWithContext:context queue:queue]; - return nil; - }]; - return contextId; -} - -static void removeContext(int32_t contextId) { - [callContexts() with:^id(NSMutableDictionary *dict) { - [dict removeObjectForKey:@(contextId)]; - return nil; - }]; -} - -static void withContext(int32_t contextId, void (^f)(OngoingCallThreadLocalContext *)) { - __block OngoingCallThreadLocalContextReference *reference = nil; - [callContexts() with:^id(NSMutableDictionary *dict) { - reference = dict[@(contextId)]; - return nil; - }]; - if (reference != nil) { - [reference.queue dispatch:^{ - __strong OngoingCallThreadLocalContext *context = reference.context; - if (context != nil) { - f(context); - } - }]; - } -} - -@interface OngoingCallThreadLocalContext () { - id _queue; - int32_t _contextId; - - OngoingCallNetworkType _networkType; - NSTimeInterval _callReceiveTimeout; - NSTimeInterval _callRingTimeout; - NSTimeInterval _callConnectTimeout; - NSTimeInterval _callPacketTimeout; - - std::unique_ptr _tgVoip; - - OngoingCallState _state; - int32_t _signalBars; - NSData *_lastDerivedState; -} - -- (void)controllerStateChanged:(TgVoipState)state; -- (void)signalBarsChanged:(int32_t)signalBars; - -@end - -@implementation VoipProxyServer - -- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password { - self = [super init]; - if (self != nil) { - _host = host; - _port = port; - _username = username; - _password = password; - } - return self; -} - -@end - -static TgVoipNetworkType callControllerNetworkTypeForType(OngoingCallNetworkType type) { - switch (type) { - case OngoingCallNetworkTypeWifi: - return TgVoipNetworkType::WiFi; - case OngoingCallNetworkTypeCellularGprs: - return TgVoipNetworkType::Gprs; - case OngoingCallNetworkTypeCellular3g: - return TgVoipNetworkType::ThirdGeneration; - case OngoingCallNetworkTypeCellularLte: - return TgVoipNetworkType::Lte; - default: - return TgVoipNetworkType::ThirdGeneration; - } -} - -static TgVoipDataSaving callControllerDataSavingForType(OngoingCallDataSaving type) { - switch (type) { - case OngoingCallDataSavingNever: - return TgVoipDataSaving::Never; - case OngoingCallDataSavingCellular: - return TgVoipDataSaving::Mobile; - case OngoingCallDataSavingAlways: - return TgVoipDataSaving::Always; - default: - return TgVoipDataSaving::Never; - } -} - -@implementation OngoingCallThreadLocalContext - -static void (*InternalVoipLoggingFunction)(NSString *) = NULL; - -+ (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction { - InternalVoipLoggingFunction = loggingFunction; - TgVoip::setLoggingFunction([](std::string const &string) { - if (InternalVoipLoggingFunction) { - InternalVoipLoggingFunction([[NSString alloc] initWithUTF8String:string.c_str()]); - } - }); -} - -+ (void)applyServerConfig:(NSString *)string { - if (string.length != 0) { - TgVoip::setGlobalServerConfig(std::string(string.UTF8String)); - } -} - -+ (int32_t)maxLayer { - return 92; -} - -+ (NSString *)version { - return [NSString stringWithUTF8String:TgVoip::getVersion().c_str()]; -} - -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType dataSaving:(OngoingCallDataSaving)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath { - self = [super init]; - if (self != nil) { - _queue = queue; - assert([queue isCurrent]); - _contextId = addContext(self, queue); - - _callReceiveTimeout = 20.0; - _callRingTimeout = 90.0; - _callConnectTimeout = 30.0; - _callPacketTimeout = 10.0; - _networkType = networkType; - - std::vector derivedStateValue; - derivedStateValue.resize(derivedState.length); - [derivedState getBytes:derivedStateValue.data() length:derivedState.length]; - - std::unique_ptr proxyValue = nullptr; - if (proxy != nil) { - TgVoipProxy *proxyObject = new TgVoipProxy(); - proxyObject->host = proxy.host.UTF8String; - proxyObject->port = (uint16_t)proxy.port; - proxyObject->login = proxy.username.UTF8String ?: ""; - proxyObject->password = proxy.password.UTF8String ?: ""; - proxyValue = std::unique_ptr(proxyObject); - } - - TgVoipCrypto crypto; - crypto.sha1 = &TGCallSha1; - crypto.sha256 = &TGCallSha256; - crypto.rand_bytes = &TGCallRandomBytes; - crypto.aes_ige_encrypt = &TGCallAesIgeEncrypt; - crypto.aes_ige_decrypt = &TGCallAesIgeDecrypt; - crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt; - - std::vector endpoints; - NSArray *connections = [@[primaryConnection] arrayByAddingObjectsFromArray:alternativeConnections]; - for (OngoingCallConnectionDescription *connection in connections) { - unsigned char peerTag[16]; - [connection.peerTag getBytes:peerTag length:16]; - - TgVoipEndpoint endpoint; - endpoint.endpointId = connection.connectionId; - endpoint.host = { - .ipv4 = std::string(connection.ip.UTF8String), - .ipv6 = std::string(connection.ipv6.UTF8String) - }; - endpoint.port = (uint16_t)connection.port; - endpoint.type = TgVoipEndpointType::UdpRelay; - memcpy(endpoint.peerTag, peerTag, 16); - endpoints.push_back(endpoint); - } - - TgVoipConfig config; - config.initializationTimeout = _callConnectTimeout; - config.receiveTimeout = _callPacketTimeout; - config.dataSaving = callControllerDataSavingForType(dataSaving); - config.enableP2P = static_cast(allowP2P); - config.enableAEC = false; - config.enableNS = true; - config.enableAGC = true; - config.enableVolumeControl = false; - config.enableCallUpgrade = false; - config.logPath = logPath.length == 0 ? "" : std::string(logPath.UTF8String); - config.maxApiLayer = [OngoingCallThreadLocalContext maxLayer]; - - std::vector encryptionKeyValue; - encryptionKeyValue.resize(key.length); - memcpy(encryptionKeyValue.data(), key.bytes, key.length); - - TgVoipEncryptionKey encryptionKey; - encryptionKey.value = encryptionKeyValue; - encryptionKey.isOutgoing = isOutgoing; - - _tgVoip = TgVoip::makeInstance( - config, - { derivedStateValue }, - endpoints, - proxyValue.get(), - callControllerNetworkTypeForType(networkType), - encryptionKey, - crypto - ); - - _state = OngoingCallStateInitializing; - _signalBars = -1; - - __weak OngoingCallThreadLocalContext *weakSelf = self; - _tgVoip->setOnStateUpdated([weakSelf](TgVoipState state) { - __strong OngoingCallThreadLocalContext *strongSelf = weakSelf; - if (strongSelf) { - [strongSelf controllerStateChanged:state]; - } - }); - _tgVoip->setOnSignalBarsUpdated([weakSelf](int signalBars) { - __strong OngoingCallThreadLocalContext *strongSelf = weakSelf; - if (strongSelf) { - [strongSelf signalBarsChanged:signalBars]; - } - }); - } - return self; -} - -- (void)dealloc { - assert([_queue isCurrent]); - removeContext(_contextId); - if (_tgVoip != NULL) { - [self stop:nil]; - } -} - -- (void)stop:(void (^)(NSString *, int64_t, int64_t, int64_t, int64_t))completion { - if (_tgVoip) { - TgVoipFinalState finalState = _tgVoip->stop(); - - NSString *debugLog = [NSString stringWithUTF8String:finalState.debugLog.c_str()]; - _lastDerivedState = [[NSData alloc] initWithBytes:finalState.persistentState.value.data() length:finalState.persistentState.value.size()]; - - _tgVoip.reset(); - - if (completion) { - completion(debugLog, finalState.trafficStats.bytesSentWifi, finalState.trafficStats.bytesReceivedWifi, finalState.trafficStats.bytesSentMobile, finalState.trafficStats.bytesReceivedMobile); - } - } -} - -- (NSString *)debugInfo { - if (_tgVoip != nil) { - auto rawDebugString = _tgVoip->getDebugInfo(); - return [NSString stringWithUTF8String:rawDebugString.c_str()]; - } else { - return nil; - } -} - -- (NSString *)version { - if (_tgVoip != nil) { - return [NSString stringWithUTF8String:_tgVoip->getVersion().c_str()]; - } else { - return nil; - } -} - -- (NSData * _Nonnull)getDerivedState { - if (_tgVoip) { - auto persistentState = _tgVoip->getPersistentState(); - return [[NSData alloc] initWithBytes:persistentState.value.data() length:persistentState.value.size()]; - } else if (_lastDerivedState != nil) { - return _lastDerivedState; - } else { - return [NSData data]; - } -} - -- (void)controllerStateChanged:(TgVoipState)state { - OngoingCallState callState = OngoingCallStateInitializing; - switch (state) { - case TgVoipState::Established: - callState = OngoingCallStateConnected; - break; - case TgVoipState::Failed: - callState = OngoingCallStateFailed; - break; - case TgVoipState::Reconnecting: - callState = OngoingCallStateReconnecting; - break; - default: - break; - } - - if (callState != _state) { - _state = callState; - - if (_stateChanged) { - _stateChanged(callState); - } - } -} - -- (void)signalBarsChanged:(int32_t)signalBars { - if (signalBars != _signalBars) { - _signalBars = signalBars; - - if (_signalBarsChanged) { - _signalBarsChanged(signalBars); - } - } -} - -- (void)setIsMuted:(bool)isMuted { - if (_tgVoip) { - _tgVoip->setMuteMicrophone(isMuted); - } -} - -- (void)setNetworkType:(OngoingCallNetworkType)networkType { - if (_networkType != networkType) { - _networkType = networkType; - if (_tgVoip) { - _tgVoip->setNetworkType(callControllerNetworkTypeForType(networkType)); - } - } -} - -@end diff --git a/submodules/TgVoip/libtgvoip b/submodules/TgVoip/libtgvoip deleted file mode 160000 index 37d98e984f..0000000000 --- a/submodules/TgVoip/libtgvoip +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 37d98e984fd6fa389262307db826d52ab86c8241 diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index d08d031a79..ca6be80c5a 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -11,6 +11,36 @@ #define UIView NSView #endif +@interface OngoingCallConnectionDescription : NSObject + +@property (nonatomic, readonly) int64_t connectionId; +@property (nonatomic, strong, readonly) NSString * _Nonnull ip; +@property (nonatomic, strong, readonly) NSString * _Nonnull ipv6; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSData * _Nonnull peerTag; + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag; + +@end + +@protocol OngoingCallThreadLocalContextQueue + +- (void)dispatch:(void (^ _Nonnull)())f; +- (bool)isCurrent; + +@end + +@interface VoipProxyServer : NSObject + +@property (nonatomic, strong, readonly) NSString * _Nonnull host; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSString * _Nullable username; +@property (nonatomic, strong, readonly) NSString * _Nullable password; + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password; + +@end + @interface CallAudioTone : NSObject @property (nonatomic, strong, readonly) NSData * _Nonnull samples; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 5955fe26b2..66d632c72e 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -42,6 +42,38 @@ #import "platform/darwin/TGRTCCVPixelBuffer.h" #include "rtc_base/logging.h" +@implementation OngoingCallConnectionDescription + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag { + self = [super init]; + if (self != nil) { + _connectionId = connectionId; + _ip = ip; + _ipv6 = ipv6; + _port = port; + _peerTag = peerTag; + } + return self; +} + +@end + +@implementation VoipProxyServer + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password { + self = [super init]; + if (self != nil) { + _host = host; + _port = port; + _username = username; + _password = password; + } + return self; +} + +@end + + @implementation CallAudioTone - (instancetype _Nonnull)initWithSamples:(NSData * _Nonnull)samples sampleRate:(NSInteger)sampleRate loopCount:(NSInteger)loopCount { @@ -1488,9 +1520,6 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; } + (void)applyServerConfig:(NSString *)string { - if (string.length != 0) { - //TgVoip::setGlobalServerConfig(std::string(string.UTF8String)); - } } + (void)setupAudioSession { diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index d50eeeb40c..1ea67f88b8 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit d50eeeb40ce6a2d36d505bcaf0b5f4e5ed473f22 +Subproject commit 1ea67f88b8d9fd04fc151d164669f6c229d651d3 diff --git a/third-party/flatc/Package.swift b/third-party/flatc/Package.swift new file mode 100644 index 0000000000..b4184bdbac --- /dev/null +++ b/third-party/flatc/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.5 +import PackageDescription + +let package = Package( + name: "FlatBuffersBuilder", + platforms: [ + .macOS(.v12) + ], + products: [ + .plugin( + name: "FlatBuffersPlugin", + targets: ["FlatBuffersPlugin"] + ) + ], + dependencies: [], + targets: [ + .binaryTarget( + name: "flatc", + url: "https://github.com/google/flatbuffers/releases/download/v23.5.26/Mac.flatc.binary.zip", + checksum: "d65628c225ef26e0386df003fe47d6b3ec8775c586d7dae1a9ef469a0a9906f1" + ), + .plugin( + name: "FlatBuffersPlugin", + capability: .buildTool(), + dependencies: ["flatc"] + ) + ] +) diff --git a/third-party/webrtc/webrtc b/third-party/webrtc/webrtc index 77d3d1fe2f..e8d2ef8c74 160000 --- a/third-party/webrtc/webrtc +++ b/third-party/webrtc/webrtc @@ -1 +1 @@ -Subproject commit 77d3d1fe2ff2f364e8edee58179a7b7b95239b01 +Subproject commit e8d2ef8c74f07c4f952db43bdfc08f39228f79c3