Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2025-02-25 19:59:58 +04:00
commit 94117d1cb1
48 changed files with 1150 additions and 1247 deletions

1
.cursorignore Normal file
View File

@ -0,0 +1 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)

1
.gitignore vendored
View File

@ -68,4 +68,5 @@ build-input/*
submodules/OpusBinding/SharedHeaders/*
submodules/FFMpegBinding/SharedHeaders/*
submodules/OpenSSLEncryptionProvider/SharedHeaders/*
submodules/TelegramCore/FlatSerialization/Sources/*
buildServer.json

3
.gitmodules vendored
View File

@ -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

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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,

View File

@ -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))
}

View File

@ -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[..<anchorRange.lowerBound])
}
if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let page = content.instantPage, page.url == baseUrl, let anchor = anchor {
if let (_, page) = self.webPage, let page, page.url == baseUrl, let anchor = anchor {
self.scrollToAnchor(anchor)
return
}
@ -1418,7 +1426,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
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
}
@ -1555,7 +1563,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
}).start()
}
}, openInSafari: { [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.context.sharedContext.applicationBindings.openUrl(content.url)
}
})

View File

@ -842,13 +842,13 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
}
}
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> 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: [])
}

View File

@ -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)
}

View File

@ -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)
}
})])

View File

@ -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) {

View File

@ -756,9 +756,14 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
self.pause()
f()
case let .loopDisablingSound(f):
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()
}
}

View File

@ -14,7 +14,7 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl
var results: [SettingsSearchableItem] = []
var nextIndex: Int32 = 2
if let resolvedUrl = resolvedUrl, case let .instantView(webPage, _) = resolvedUrl {
if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage {
if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage?._parse() {
var processingQuestions = false
var currentSection: String?
outer: for block in instantPage.blocks {

View File

@ -1128,7 +1128,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
var sharedAudioContext = sharedAudioContext
if sharedAudioContext == nil {
var useSharedAudio = true
var useSharedAudio = !isStream
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_group_shared_audio"] != nil {
useSharedAudio = false
}

View File

@ -0,0 +1,27 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FlatBuffers",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "FlatBuffers",
targets: ["FlatBuffers"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "FlatBuffers",
dependencies: [],
path: "Sources"),
]
)

View File

@ -0,0 +1,30 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FlatSerialization",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "FlatSerialization",
targets: ["FlatSerialization"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "FlatBuffers", path: "../FlatBuffers")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "FlatSerialization",
dependencies: [
.product(name: "FlatBuffers", package: "FlatBuffers", condition: nil),
],
path: "Sources"),
]
)

View File

@ -3,10 +3,15 @@
# Default directories
OUTPUT_DIR=""
INPUT_DIR=""
BINARY_PATH=""
# Parse command line arguments
while [ "$#" -gt 0 ]; do
case "$1" in
--binary)
BINARY_PATH="$2"
shift 2
;;
--output)
OUTPUT_DIR="$2"
shift 2
@ -28,6 +33,12 @@ if [ -z "$OUTPUT_DIR" ]; then
exit 1
fi
# Validate output directory
if [ -z "$BINARY_PATH" ]; then
echo "Error: --binary argument is required"
exit 1
fi
if [ ! -d "$OUTPUT_DIR" ]; then
echo "Error: Output directory does not exist: $OUTPUT_DIR"
exit 1
@ -58,4 +69,4 @@ for model in $models; do
flatc_input="$flatc_input $model"
done
flatc --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input}
$BINARY_PATH --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input}

View File

@ -15,6 +15,8 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "FlatBuffers", path: "./FlatBuffers"),
.package(name: "FlatSerialization", path: "./FlatSerialization"),
.package(name: "Postbox", path: "../Postbox"),
.package(name: "SSignalKit", path: "../SSignalKit"),
.package(name: "MtProtoKit", path: "../MtProtoKit"),
@ -40,6 +42,8 @@ let package = Package(
.product(name: "DarwinDirStat", package: "DarwinDirStat", condition: nil),
.product(name: "Reachability", package: "Reachability", condition: nil),
.product(name: "Emoji", package: "Emoji", condition: nil),
.product(name: "FlatBuffers", package: "FlatBuffers", condition: nil),
.product(name: "FlatSerialization", package: "FlatSerialization", condition: nil),
.product(name: "EncryptionProvider", package: "EncryptionProvider", condition: nil)],
path: "Sources"),
]

View File

@ -2,7 +2,6 @@ import Foundation
import Postbox
import TelegramApi
func parsedTelegramProfilePhoto(_ photo: Api.UserProfilePhoto) -> [TelegramMediaImageRepresentation] {
var representations: [TelegramMediaImageRepresentation] = []
switch photo {

View File

@ -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

View File

@ -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
}
}

View File

@ -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("")
}
}
}

View File

@ -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<MediaId, Media>.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
}
}

View File

@ -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 {
encoder.encodeNil(forKey: "ip")
preconditionFailure()
}
} else {
encoder.encodeNil(forKey: "ipd")
}
}
}

View File

@ -857,6 +857,38 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct PeerSettings: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Optional<PeerStatusSettings>
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

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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<Bool, NoError>
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
}

View File

@ -198,11 +198,6 @@ public extension EmojiPagerContentComponent {
searchCategories = .single(nil)
}
#if DEBUG || true
var isFirstTime = true
let measure_startTime = CFAbsoluteTimeGetCurrent()
#endif
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = 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

View File

@ -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,

View File

@ -505,6 +505,7 @@ public final class MessageInputPanelComponent: Component {
private var viewForOverlayContent: ViewForOverlayContent?
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var currentEmojiSearchView: ComponentHostView<Empty>?
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)
),
@ -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<Empty>
if let current = self.currentEmojiSearchView {
currentEmojiSearchView = current
} else {
currentEmojiSearchView = ComponentHostView<Empty>()
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
}
}

View File

@ -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 {

View File

@ -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<EnginePeer?, NoError> 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)

View File

@ -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,

View File

@ -189,6 +189,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return self.hasGroupCallOnScreenPromise.get()
}
private var streamController: MediaStreamComponentController?
private var immediateHasOngoingCallValue = Atomic<Bool>(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(),

View File

@ -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",

View File

@ -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<UInt8>(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)]
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<NetworkType, NoError>, 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<Bool, NoError>, logName: String, preferredVideoCodec: String?, audioDevice: AudioDevice?) {
let _ = setupLogs
OngoingCallThreadLocalContext.applyServerConfig(serializedData)
self.callId = callId
self.internalId = internalId
@ -1068,18 +923,8 @@ 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()
}
if useModernImplementation {
var voipProxyServer: VoipProxyServerWebrtc?
if let proxyServer = proxyServer {
switch proxyServer.connection {
@ -1346,33 +1191,6 @@ public final class OngoingCallContext {
context.nativeSetNetworkType(networkType)
}
})
} 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 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)
}
})
}
strongSelf.signalingDataDisposable = callSessionManager.beginReceivingCallSignalingData(internalId: internalId, { [weak self] dataList in
queue.async {

View File

@ -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",
],
)

View File

@ -1,81 +0,0 @@
#ifndef OngoingCallContext_h
#define OngoingCallContext_h
#import <Foundation/Foundation.h>
@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 <NSObject>
- (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<OngoingCallThreadLocalContextQueue> _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<OngoingCallConnectionDescription *> * _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

View File

@ -1,419 +0,0 @@
#import "OngoingCallThreadLocalContext.h"
#import "TgVoip.h"
#import <MtProtoKit/MtProtoKit.h>
#include <memory>
#import <libkern/OSAtomic.h>
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<OngoingCallThreadLocalContextQueue> queue;
@end
@implementation OngoingCallThreadLocalContextReference
- (instancetype)initWithContext:(OngoingCallThreadLocalContext *)context queue:(id<OngoingCallThreadLocalContextQueue>)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<OngoingCallThreadLocalContextQueue> 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<OngoingCallThreadLocalContextQueue> _queue;
int32_t _contextId;
OngoingCallNetworkType _networkType;
NSTimeInterval _callReceiveTimeout;
NSTimeInterval _callRingTimeout;
NSTimeInterval _callConnectTimeout;
NSTimeInterval _callPacketTimeout;
std::unique_ptr<TgVoip> _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<OngoingCallThreadLocalContextQueue> _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<OngoingCallConnectionDescription *> * _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<uint8_t> derivedStateValue;
derivedStateValue.resize(derivedState.length);
[derivedState getBytes:derivedStateValue.data() length:derivedState.length];
std::unique_ptr<TgVoipProxy> 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<TgVoipProxy>(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<TgVoipEndpoint> endpoints;
NSArray<OngoingCallConnectionDescription *> *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<bool>(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<uint8_t> 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

@ -1 +0,0 @@
Subproject commit 37d98e984fd6fa389262307db826d52ab86c8241

View File

@ -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 <NSObject>
- (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;

View File

@ -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 {

@ -1 +1 @@
Subproject commit d50eeeb40ce6a2d36d505bcaf0b5f4e5ed473f22
Subproject commit 1ea67f88b8d9fd04fc151d164669f6c229d651d3

28
third-party/flatc/Package.swift vendored Normal file
View File

@ -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"]
)
]
)

@ -1 +1 @@
Subproject commit 77d3d1fe2ff2f364e8edee58179a7b7b95239b01
Subproject commit e8d2ef8c74f07c4f952db43bdfc08f39228f79c3