diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index ebf196ee09..30712dcc44 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13773,3 +13773,9 @@ Sorry for the inconvenience."; "Gift.Hidden.ChannelText" = "The gift is removed from the channel's Page."; "Gift.Upgrade.AddChannelName" = "Add channel name to the gift"; + +"AffiliateProgram.OpenBot" = "View %@"; + +"AvatarUpload.StatusUploading" = "Your photo is uploading."; +"AvatarUpload.StatusDone" = "Your photo is now set."; +"AvatarUpload.ViewAction" = "View"; diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 3cff68fb62..d88b32285c 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -143,6 +143,9 @@ public protocol PresentationCall: AnyObject { var state: Signal { get } var audioLevel: Signal { get } + + var hasConference: Signal { get } + var conferenceCall: PresentationGroupCall? { get } var isMuted: Signal { get } @@ -164,7 +167,7 @@ public protocol PresentationCall: AnyObject { func setCurrentAudioOutput(_ output: AudioSessionOutput) func debugInfo() -> Signal<(String, String), NoError> - func createConferenceIfPossible() + func upgradeToConference(completion: @escaping (PresentationGroupCall) -> Void) -> Disposable func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) } diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift index 3281342e1f..19281cc1fc 100644 --- a/submodules/AccountContext/Sources/ShareController.swift +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -49,12 +49,20 @@ public enum ShareControllerError { } public enum ShareControllerSubject { + public final class MediaParameters { + public let startAtTimestamp: Int32? + + public init(startAtTimestamp: Int32?) { + self.startAtTimestamp = startAtTimestamp + } + } + case url(String) case text(String) case quote(text: String, url: String) case messages([Message]) case image([ImageRepresentationWithReference]) - case media(AnyMediaReference) + case media(AnyMediaReference, MediaParameters?) case mapMedia(TelegramMediaMap) case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal) } diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index f4fc4b03ff..0a46ee4a66 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -569,10 +569,10 @@ public class BrowserScreen: ViewController, MinimizableController { var isDocument = false if let content = self.content.last { if let documentContent = content as? BrowserDocumentContent { - subject = .media(documentContent.file.abstract) + subject = .media(documentContent.file.abstract, nil) isDocument = true } else if let documentContent = content as? BrowserPdfContent { - subject = .media(documentContent.file.abstract) + subject = .media(documentContent.file.abstract, nil) isDocument = true } else { subject = .url(url) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 8105752585..03ad7bd696 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -674,7 +674,6 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance) } - //TODO:localize entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium)) entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium)) entries.append(.tagColorFooter) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index ba10231f7e..b8056bc807 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -759,7 +759,7 @@ open class NavigationController: UINavigationController, ContainableController, } let effectiveModalTransition: CGFloat - if visibleModalCount == 0 || navigationLayout.modal[i].isFlat { + if visibleModalCount == 0 || (navigationLayout.modal[i].isFlat && !navigationLayout.modal[i].flatReceivesModalTransition) { effectiveModalTransition = 0.0 } else if visibleModalCount == 1 { effectiveModalTransition = 1.0 - topModalDismissProgress diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index a2a123c073..a4fddcfcc8 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -11,6 +11,7 @@ enum RootNavigationLayout { struct ModalContainerLayout { var controllers: [ViewController] var isFlat: Bool + var flatReceivesModalTransition: Bool var isStandalone: Bool } @@ -26,6 +27,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL let requiresModal: Bool var beginsModal: Bool = false var isFlat: Bool = false + var flatReceivesModalTransition: Bool = false var isStandalone: Bool = false switch controller.navigationPresentation { case .default: @@ -39,6 +41,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL requiresModal = true beginsModal = true isFlat = true + flatReceivesModalTransition = controller.flatReceivesModalTransition case .standaloneModal: requiresModal = true beginsModal = true @@ -68,7 +71,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL if requiresModal { controller._presentedInModal = true if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone { - modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) + modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone)) } else { modalStack[modalStack.count - 1].controllers.append(controller) } @@ -78,7 +81,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL controller._presentedInModal = true } if modalStack[modalStack.count - 1].isStandalone { - modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) + modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone)) } else { modalStack[modalStack.count - 1].controllers.append(controller) } diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 5e09c77e07..3a86497e78 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -342,9 +342,16 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.validLayout = layout var isStandaloneModal = false - if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation { - isStandaloneModal = true + var flatReceivesModalTransition = false + if let controller = controllers.first { + if case .standaloneModal = controller.navigationPresentation { + isStandaloneModal = true + } + if controller.flatReceivesModalTransition { + flatReceivesModalTransition = true + } } + let _ = flatReceivesModalTransition transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) self.ignoreScrolling = true @@ -378,7 +385,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes } else { self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) } - if isStandaloneModal || isLandscape || self.isFlat { + if isStandaloneModal || isLandscape || (self.isFlat && !flatReceivesModalTransition) { self.container.cornerRadius = 0.0 } else { self.container.cornerRadius = 10.0 @@ -419,13 +426,19 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size) let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition - let maxScaledTopInset: CGFloat = topInset - 10.0 + var maxScaledTopInset: CGFloat = topInset - 10.0 + if flatReceivesModalTransition { + maxScaledTopInset = 0.0 + if let statusBarHeight = layout.statusBarHeight { + maxScaledTopInset += statusBarHeight + } + } let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) } } else { self.panRecognizer?.isEnabled = false - if self.isFlat { + if self.isFlat && !flatReceivesModalTransition { self.dim.backgroundColor = .clear self.container.clipsToBounds = true self.container.cornerRadius = 0.0 diff --git a/submodules/Display/Source/TabBarController.swift b/submodules/Display/Source/TabBarController.swift index 2c09401ee1..e552c5fafe 100644 --- a/submodules/Display/Source/TabBarController.swift +++ b/submodules/Display/Source/TabBarController.swift @@ -27,4 +27,6 @@ public protocol TabBarController: ViewController { func updateIsTabBarEnabled(_ value: Bool, transition: ContainedViewLayoutTransition) func updateIsTabBarHidden(_ value: Bool, transition: ContainedViewLayoutTransition) func updateLayout(transition: ContainedViewLayoutTransition) + + func updateControllerLayout(controller: ViewController) } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index aaf2778854..90c53ba32c 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -163,6 +163,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open var navigationPresentation: ViewControllerNavigationPresentation = .default open var _presentedInModal: Bool = false + open var flatReceivesModalTransition: Bool = false public var presentedOverCoveringView: Bool = false diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 49e2103cc1..5d1067eb15 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -186,6 +186,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var interacting: ((Bool) -> Void)? + var shareMediaParameters: (() -> ShareControllerSubject.MediaParameters?)? + private var seekTimer: SwiftSignalKit.Timer? private var currentIsPaused: Bool = true private var seekRate: Double = 1.0 @@ -1644,16 +1646,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } else { if let file = content.file { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: file)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), nil) preferredAction = .saveToCameraRoll } else if let image = content.image { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: image)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), nil) preferredAction = .saveToCameraRoll actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved } } } else if let file = m as? TelegramMediaFile { - subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file)) + subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file), strongSelf.shareMediaParameters?()) if file.isAnimated { if messages[0].id.peerId.namespace == Namespaces.Peer.SecretChat { preferredAction = .default @@ -1666,7 +1668,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controllerInteraction = strongSelf.controllerInteraction let _ = (toggleGifSaved(account: context.account, fileReference: .message(message: MessageReference(message._asMessage()), media: file), saved: true) - |> deliverOnMainQueue).start(next: { result in + |> deliverOnMainQueue).start(next: { result in switch result { case .generic: controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) @@ -1873,7 +1875,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } var preferredAction = ShareControllerPreferredAction.default - var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media)) + var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media), self.shareMediaParameters?()) if let file = media as? TelegramMediaFile { if file.isAnimated { @@ -1935,10 +1937,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } else { if let file = content.file { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: file)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), self.shareMediaParameters?()) preferredAction = .saveToCameraRoll } else if let image = content.image { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: image)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), self.shareMediaParameters?()) preferredAction = .saveToCameraRoll } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 5d47d50928..677647b671 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1404,6 +1404,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.clipsToBounds = true + //TODO:wip-release + /*self.footerContentNode.shareMediaParameters = { [weak self] in + guard let self, let playerStatusValue = self.playerStatusValue else { + return nil + } + if playerStatusValue.duration >= 60.0 * 10.0 { + return ShareControllerSubject.MediaParameters(startAtTimestamp: Int32(playerStatusValue.timestamp)) + } else { + return nil + } + }*/ + self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) @@ -1842,7 +1854,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { disablePictureInPicture = true } else if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.namespace == Namespaces.Message.Local { disablePictureInPicture = true - } else { + } + + if message.paidContent == nil { let throttledSignal = videoNode.status |> mapToThrottled { next -> Signal in return .single(next) |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue())) @@ -2390,6 +2404,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { seek = .timecode(time) } } + + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { + for attribute in message.attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + seek = .timecode(Double(attribute.timestamp)) + } + } + } } videoNode.setBaseRate(self.playbackRate ?? 1.0) diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift index e55f6ffdea..9b419ded63 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift @@ -147,7 +147,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { @objc func actionButtonPressed() { if let shareMedia = self.shareMedia { - self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil) + self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia, nil), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil) } } } diff --git a/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift b/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift index 69ec0dcfc3..bdb545a9e2 100644 --- a/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift +++ b/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import TelegramCore import TelegramUIPreferences +import Postbox public final class MediaPlaybackStoredState: Codable { public let timestamp: Double @@ -29,28 +30,28 @@ public final class MediaPlaybackStoredState: Codable { } public func mediaPlaybackStoredState(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal { - let key = EngineDataBuffer(length: 20) - key.setInt32(0, value: messageId.namespace) - key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value()) - key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value()) - key.setInt32(16, value: messageId.id) - - return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key)) - |> map { entry -> MediaPlaybackStoredState? in - return entry?.get(MediaPlaybackStoredState.self) + return engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> map { message -> MediaPlaybackStoredState? in + guard let message else { + return nil + } + for attribute in message.attributes { + if let attribute = attribute as? DerivedDataMessageAttribute { + return attribute.data["mps"]?.get(MediaPlaybackStoredState.self) + } + } + return nil } } public func updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine, messageId: EngineMessage.Id, state: MediaPlaybackStoredState?) -> Signal { - let key = EngineDataBuffer(length: 20) - key.setInt32(0, value: messageId.namespace) - key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value()) - key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value()) - key.setInt32(16, value: messageId.id) - - if let state = state { - return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key, item: state) - } else { - return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key) - } + return engine.messages.updateLocallyDerivedData(messageId: messageId, update: { data in + var data = data + if let state, let entry = CodableEntry(state) { + data["mps"] = entry + } else { + data.removeValue(forKey: "mps") + } + return data + }) } diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index de69497b10..3d29daa132 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -188,7 +188,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { var actionCompletionText: String? if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) - subject = .media(videoFileReference.abstract) + subject = .media(videoFileReference.abstract, nil) actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved } else { subject = .image(entry.representations) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index d000c3da2b..2713ca2a34 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -521,7 +521,7 @@ public final class ThemePreviewController: ViewController { subject = .url("https://t.me/addtheme/\(slug)") preferredAction = .default case let .media(media): - subject = .media(media) + subject = .media(media, nil) preferredAction = .default } let controller = ShareController(context: self.context, subject: subject, preferredAction: preferredAction) diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 3f541667da..f20bc90047 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramUI/Components/MessageInputPanelComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/ChatPresentationInterfaceState", + "//submodules/CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareActionButtonNode.swift b/submodules/ShareController/Sources/ShareActionButtonNode.swift index 8be8ba0a98..c63b3df2ae 100644 --- a/submodules/ShareController/Sources/ShareActionButtonNode.swift +++ b/submodules/ShareController/Sources/ShareActionButtonNode.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import ContextUI +import CheckNode public final class ShareActionButtonNode: HighlightTrackingButtonNode { private let referenceNode: ContextReferenceContentNode @@ -109,3 +110,74 @@ public final class ShareActionButtonNode: HighlightTrackingButtonNode { self.referenceNode.frame = self.bounds } } + +public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode { + private let checkNode: CheckNode + private let titleTextNode: TextNode + + public var titleTextColor: UIColor { + didSet { + self.setNeedsLayout() + } + } + public var checkNodeTheme: CheckNodeTheme { + didSet { + self.checkNode.theme = self.checkNodeTheme + } + } + + private let titleText: String + + public var value: Bool { + return self.checkNode.selected + } + + public init(titleText: String, titleTextColor: UIColor, checkNodeTheme: CheckNodeTheme) { + self.titleText = titleText + self.titleTextColor = titleTextColor + self.checkNodeTheme = checkNodeTheme + + self.checkNode = CheckNode(theme: checkNodeTheme, content: .check) + self.checkNode.isUserInteractionEnabled = false + + self.titleTextNode = TextNode() + self.titleTextNode.isUserInteractionEnabled = false + self.titleTextNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.checkNode) + self.addSubnode(self.titleTextNode) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.checkNode.setSelected(!self.checkNode.selected, animated: true) + } + + override public func layout() { + super.layout() + + if self.bounds.width < 1.0 { + return + } + + let checkSize: CGFloat = 18.0 + let checkSpacing: CGFloat = 10.0 + + let (titleTextLayout, titleTextApply) = TextNode.asyncLayout(self.titleTextNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.titleText, font: Font.regular(13.0), textColor: self.titleTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: self.bounds.width - 8.0 * 2.0 - checkSpacing - checkSize, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let _ = titleTextApply() + + let contentWidth = checkSize + checkSpacing + titleTextLayout.size.width + let checkFrame = CGRect(origin: CGPoint(x: floor((self.bounds.width - contentWidth) * 0.5), y: floor((self.bounds.height - checkSize) * 0.5)), size: CGSize(width: checkSize, height: checkSize)) + + let isFirstTime = self.checkNode.bounds.isEmpty + self.checkNode.frame = checkFrame + if isFirstTime { + self.checkNode.setSelected(false, animated: false) + } + + self.titleTextNode.frame = CGRect(origin: CGPoint(x: checkFrame.maxX + checkSpacing, y: floor((self.bounds.height - titleTextLayout.size.height) * 0.5)), size: titleTextLayout.size) + } +} diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 27bb68504b..ff18c24765 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -524,7 +524,7 @@ public final class ShareController: ViewController { self?.actionCompleted?() }) } - case let .media(mediaReference): + case let .media(mediaReference, _): var canSave = false var isVideo = false if mediaReference.media is TelegramMediaImage { @@ -668,7 +668,12 @@ public final class ShareController: ViewController { fromPublicChannel = true } - self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in + var mediaParameters: ShareControllerSubject.MediaParameters? + if case let .media(_, parameters) = self.subject { + mediaParameters = parameters + } + + self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, mediaParameters: mediaParameters, requestLayout: { [weak self] transition in self?.requestLayout(transition: transition) }, presentError: { [weak self] title, text in guard let strongSelf = self else { @@ -765,7 +770,7 @@ public final class ShareController: ViewController { return false } } - case let .media(mediaReference): + case let .media(mediaReference, _): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -981,7 +986,7 @@ public final class ShareController: ViewController { case let .image(representations): let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: .standalone(media: media))) - case let .media(mediaReference): + case let .media(mediaReference, _): collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: mediaReference)) case let .mapMedia(media): let latLong = "\(media.latitude),\(media.longitude)" @@ -1518,7 +1523,7 @@ public final class ShareController: ViewController { messages: messages )) } - case let .media(mediaReference): + case let .media(mediaReference, _): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -2041,7 +2046,7 @@ public final class ShareController: ViewController { messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } - case let .media(mediaReference): + case let .media(mediaReference, mediaParameters): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -2116,7 +2121,15 @@ public final class ShareController: ViewController { if !text.isEmpty && !sendTextAsCaption { messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + var attributes: [MessageAttribute] = [] + if let startAtTimestamp = mediaParameters?.startAtTimestamp, let startAtTimestampNode = strongSelf.controllerNode.startAtTimestampNode, startAtTimestampNode.value { + attributes.append(ForwardVideoTimestampAttribute(timestamp: startAtTimestamp)) + } + if case let .message(message, _) = mediaReference, let sourceMessageId = message.id, (sourceMessageId.peerId.namespace == Namespaces.Peer.CloudUser || sourceMessageId.peerId.namespace == Namespaces.Peer.CloudGroup || sourceMessageId.peerId.namespace == Namespaces.Peer.CloudChannel) { + messages.append(.forward(source: sourceMessageId, threadId: threadId, grouping: .auto, attributes: attributes, correlationId: nil)) + } else { + messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index b3fb9e33ed..0da8a2d193 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -14,6 +14,7 @@ import MultilineTextComponent import TelegramStringFormatting import BundleIconComponent import LottieComponent +import CheckNode enum ShareState { case preparing(Bool) @@ -296,6 +297,22 @@ private final class ShareContentInfoView: UIView { } } +private func textForTimeout(value: Int32) -> String { + if value < 3600 { + let minutes = value / 60 + let seconds = value % 60 + let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutes):\(secondsPadding)\(seconds)" + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let minutesPadding = minutes < 10 ? "0" : "" + let seconds = value % 60 + let secondsPadding = seconds < 10 ? "0" : "" + return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)" + } +} + final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { private weak var controller: ShareController? private let environment: ShareControllerEnvironment @@ -332,6 +349,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let actionsBackgroundNode: ASImageNode private let actionButtonNode: ShareActionButtonNode + let startAtTimestampNode: ShareStartAtTimestampNode? private let inputFieldNode: ShareInputFieldNode private let actionSeparatorNode: ASDisplayNode @@ -366,7 +384,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let showNames = ValuePromise(true) - init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { + init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { self.controller = controller self.environment = environment self.presentationData = presentationData @@ -446,6 +464,13 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.actionButtonNode.titleNode.displaysAsynchronously = false self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + if let startAtTimestamp = mediaParameters?.startAtTimestamp { + //TODO:localize + self.startAtTimestampNode = ShareStartAtTimestampNode(titleText: "Start at \(textForTimeout(value: startAtTimestamp))", titleTextColor: self.presentationData.theme.actionSheet.secondaryTextColor, checkNodeTheme: CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false)) + } else { + self.startAtTimestampNode = nil + } + self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment) self.inputFieldNode.text = presetText ?? "" self.inputFieldNode.preselectText() @@ -650,6 +675,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.contentContainerNode.addSubnode(self.actionsBackgroundNode) self.contentContainerNode.addSubnode(self.inputFieldNode) self.contentContainerNode.addSubnode(self.actionButtonNode) + if let startAtTimestampNode = self.startAtTimestampNode { + self.contentContainerNode.addSubnode(startAtTimestampNode) + } self.inputFieldNode.updateHeight = { [weak self] in if let strongSelf = self { @@ -844,6 +872,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.actionButtonNode.badgeBackgroundColor = presentationData.theme.actionSheet.controlAccentColor self.actionButtonNode.badgeTextColor = presentationData.theme.actionSheet.opaqueItemBackgroundColor + if let startAtTimestampNode = self.startAtTimestampNode { + startAtTimestampNode.titleTextColor = presentationData.theme.actionSheet.secondaryTextColor + startAtTimestampNode.checkNodeTheme = CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false) + } + self.contentNode?.updateTheme(presentationData.theme) } @@ -992,6 +1025,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate actionButtonHeight = buttonHeight bottomGridInset += actionButtonHeight } + if self.startAtTimestampNode != nil { + bottomGridInset += buttonHeight + } let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition) if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil { @@ -1013,6 +1049,10 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + if let startAtTimestampNode = self.startAtTimestampNode { + transition.updateFrame(node: startAtTimestampNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + } + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: inputHeight)), beginWithCurrentState: true) transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)), beginWithCurrentState: true) @@ -1563,6 +1603,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) { return result } + if let startAtTimestampNode = self.startAtTimestampNode { + if let result = startAtTimestampNode.hitTest(startAtTimestampNode.convert(point, from: self), with: event) { + return result + } + } if self.bounds.contains(point) { if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { if let result = contentInfoView.hitTest(self.view.convert(point, to: contentInfoView), with: event) { diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 1d4d9439c0..3b025e3a6a 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -363,7 +363,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentOffsetUpdated = f } - private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) { + private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) { let itemCount = self.entries.count let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) @@ -384,7 +384,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) - let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0) + let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset) return (gridTopInset, itemWidth) } @@ -572,7 +572,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.overrideGridOffsetTransition = nil } - let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset) var scrollToItem: GridNodeScrollToItem? if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 13b171ace0..091b864989 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -454,6 +454,35 @@ open class TabBarControllerImpl: ViewController, TabBarController { } } + public func updateControllerLayout(controller: ViewController) { + guard let layout = self.validLayout else { + return + } + if self.controllers.contains(where: { $0 === controller }) { + let currentController = controller + currentController.view.frame = CGRect(origin: CGPoint(), size: layout.size) + + var updatedLayout = layout + + var tabBarHeight: CGFloat + var options: ContainerViewLayoutInsetOptions = [] + if updatedLayout.metrics.widthClass == .regular { + options.insert(.input) + } + let bottomInset: CGFloat = updatedLayout.insets(options: options).bottom + if !updatedLayout.safeInsets.left.isZero { + tabBarHeight = 34.0 + bottomInset + } else { + tabBarHeight = 49.0 + bottomInset + } + if !self.tabBarControllerNode.tabBarHidden { + updatedLayout.intrinsicInsets.bottom = tabBarHeight + } + + currentController.containerLayoutUpdated(updatedLayout, transition: .immediate) + } + } + override open func navigationStackConfigurationUpdated(next: [ViewController]) { super.navigationStackConfigurationUpdated(next: next) for controller in self.controllers { diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 67cb22a211..15565d04ea 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -13,6 +13,7 @@ import AccountContext import TelegramNotices import AppBundle import TooltipUI +import CallScreen protocol CallControllerNodeProtocol: AnyObject { var isMuted: Bool { get set } @@ -26,6 +27,7 @@ protocol CallControllerNodeProtocol: AnyObject { var presentCallRating: ((CallId, Bool) -> Void)? { get set } var present: ((ViewController) -> Void)? { get set } var callEnded: ((Bool) -> Void)? { get set } + var willBeDismissedInteractively: (() -> Void)? { get set } var dismissedInteractively: (() -> Void)? { get set } var dismissAllTooltips: (() -> Void)? { get set } @@ -41,193 +43,6 @@ protocol CallControllerNodeProtocol: AnyObject { } public final class CallController: ViewController { - public enum Call: Equatable { - case call(PresentationCall) - case groupCall(PresentationGroupCall) - - public static func ==(lhs: Call, rhs: Call) -> Bool { - switch lhs { - case let .call(lhsCall): - if case let .call(rhsCall) = rhs { - return lhsCall === rhsCall - } else { - return false - } - case let .groupCall(lhsGroupCall): - if case let .groupCall(rhsGroupCall) = rhs { - return lhsGroupCall === rhsGroupCall - } else { - return false - } - } - } - - public var context: AccountContext { - switch self { - case let .call(call): - return call.context - case let .groupCall(groupCall): - return groupCall.accountContext - } - } - - public var peerId: EnginePeer.Id? { - switch self { - case let .call(call): - return call.peerId - case let .groupCall(groupCall): - return groupCall.peerId - } - } - - public func requestVideo() { - switch self { - case let .call(call): - call.requestVideo() - case let .groupCall(groupCall): - groupCall.requestVideo() - } - } - - public func disableVideo() { - switch self { - case let .call(call): - call.disableVideo() - case let .groupCall(groupCall): - groupCall.disableVideo() - } - } - - public func disableScreencast() { - switch self { - case let .call(call): - (call as? PresentationCallImpl)?.disableScreencast() - case let .groupCall(groupCall): - groupCall.disableScreencast() - } - } - - public func switchVideoCamera() { - switch self { - case let .call(call): - call.switchVideoCamera() - case let .groupCall(groupCall): - groupCall.switchVideoCamera() - } - } - - public func toggleIsMuted() { - switch self { - case let .call(call): - call.toggleIsMuted() - case let .groupCall(groupCall): - groupCall.toggleIsMuted() - } - } - - public func setCurrentAudioOutput(_ output: AudioSessionOutput) { - switch self { - case let .call(call): - call.setCurrentAudioOutput(output) - case let .groupCall(groupCall): - groupCall.setCurrentAudioOutput(output) - } - } - - public var isMuted: Signal { - switch self { - case let .call(call): - return call.isMuted - case let .groupCall(groupCall): - return groupCall.isMuted - } - } - - public var audioLevel: Signal { - switch self { - case let .call(call): - return call.audioLevel - case let .groupCall(groupCall): - var audioLevelId: UInt32? - return groupCall.audioLevels |> map { audioLevels -> Float in - var result: Float = 0 - for item in audioLevels { - if let audioLevelId { - if item.1 == audioLevelId { - result = item.2 - break - } - } else { - if item.1 != 0 { - audioLevelId = item.1 - result = item.2 - break - } - } - } - - return result - } - } - } - - public var isOutgoing: Bool { - switch self { - case let .call(call): - return call.isOutgoing - case .groupCall: - return false - } - } - - public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) { - switch self { - case let .call(call): - call.makeOutgoingVideoView(completion: completion) - case let .groupCall(groupCall): - groupCall.makeOutgoingVideoView(requestClone: false, completion: { a, _ in - completion(a) - }) - } - } - - public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { - switch self { - case let .call(call): - return call.audioOutputState - case let .groupCall(groupCall): - return groupCall.audioOutputState - } - } - - public func debugInfo() -> Signal<(String, String), NoError> { - switch self { - case let .call(call): - return call.debugInfo() - case .groupCall: - return .single(("", "")) - } - } - - public func answer() { - switch self { - case let .call(call): - call.answer() - case .groupCall: - break - } - } - - public func hangUp() -> Signal { - switch self { - case let .call(call): - return call.hangUp() - case let .groupCall(groupCall): - return groupCall.leave(terminateIfPossible: false) - } - } - } - private var controllerNode: CallControllerNodeProtocol { return self.displayNode as! CallControllerNodeProtocol } @@ -242,7 +57,7 @@ public final class CallController: ViewController { private let sharedContext: SharedAccountContext private let account: Account - public let call: CallController.Call + public let call: PresentationCall private let easyDebugAccess: Bool private var presentationData: PresentationData @@ -254,7 +69,7 @@ public final class CallController: ViewController { private var disposable: Disposable? private var callMutedDisposable: Disposable? - private var isMuted = false + private var isMuted: Bool = false private var presentedCallRating = false @@ -268,7 +83,10 @@ public final class CallController: ViewController { public var onViewDidAppear: (() -> Void)? public var onViewDidDisappear: (() -> Void)? - public init(sharedContext: SharedAccountContext, account: Account, call: CallController.Call, easyDebugAccess: Bool) { + private var isAnimatingDismiss: Bool = false + private var isDismissed: Bool = false + + public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) { self.sharedContext = sharedContext self.account = account self.call = call @@ -278,6 +96,12 @@ public final class CallController: ViewController { super.init(navigationBarPresentationData: nil) + if let data = call.context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_modalcalls"] != nil { + } else { + self.navigationPresentation = .flatModal + self.flatReceivesModalTransition = true + } + self._ready.set(combineLatest(queue: .mainQueue(), self.isDataReady.get(), self.isContentsReady.get()) |> map { a, b -> Bool in return a && b @@ -293,84 +117,10 @@ public final class CallController: ViewController { self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) - switch call { - case let .call(call): - self.disposable = (call.state - |> deliverOnMainQueue).start(next: { [weak self] callState in - self?.callStateUpdated(callState) - }) - case let .groupCall(groupCall): - let accountPeerId = groupCall.account.peerId - let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = groupCall.members - |> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in - guard let members else { - return (nil, nil) - } - var local: String? - var remote: PresentationGroupCallRequestedVideo? - for participant in members.participants { - if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) { - if participant.peer.id == accountPeerId { - local = video.endpointId - } else { - if remote == nil { - remote = video - } - } - } - } - return (local, remote) - } - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs == rhs - }) - - var startTimestamp: Double? - self.disposable = (combineLatest(queue: .mainQueue(), - groupCall.state, - videoEndpoints - ) - |> deliverOnMainQueue).start(next: { [weak self] callState, videoEndpoints in - guard let self else { - return - } - let mappedState: PresentationCallState.State - switch callState.networkState { - case .connecting: - mappedState = .connecting(nil) - case .connected: - let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent() - startTimestamp = timestamp - mappedState = .active(timestamp, nil, Data()) - } - - var mappedLocalVideoState: PresentationCallState.VideoState = .inactive - var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive - - if let local = videoEndpoints.local { - mappedLocalVideoState = .active(isScreencast: false, endpointId: local) - } - if let remote = videoEndpoints.remote { - mappedRemoteVideoState = .active(endpointId: remote.endpointId) - } - - if case let .groupCall(groupCall) = self.call { - var requestedVideo: [PresentationGroupCallRequestedVideo] = [] - if let remote = videoEndpoints.remote { - requestedVideo.append(remote) - } - groupCall.setRequestedVideoList(items: requestedVideo) - } - - self.callStateUpdated(PresentationCallState( - state: mappedState, - videoState: mappedLocalVideoState, - remoteVideoState: mappedRemoteVideoState, - remoteAudioState: .active, - remoteBatteryLevel: .normal - )) - }) - } + self.disposable = (call.state + |> deliverOnMainQueue).start(next: { [weak self] callState in + self?.callStateUpdated(callState) + }) self.callMutedDisposable = (call.isMuted |> deliverOnMainQueue).start(next: { [weak self] value in @@ -596,20 +346,22 @@ public final class CallController: ViewController { } } + self.controllerNode.willBeDismissedInteractively = { [weak self] in + guard let self else { + return + } + self.notifyDismissed() + } self.controllerNode.dismissedInteractively = { [weak self] in guard let self else { return } self.didPlayPresentationAnimation = false - self.presentingViewController?.dismiss(animated: false, completion: nil) + self.superDismiss() } let callPeerView: Signal - if let peerId = self.call.peerId { - callPeerView = self.account.postbox.peerView(id: peerId) |> map(Optional.init) - } else { - callPeerView = .single(nil) - } + callPeerView = self.account.postbox.peerView(id: self.call.peerId) |> map(Optional.init) self.peerDisposable = (combineLatest(queue: .mainQueue(), self.account.postbox.peerView(id: self.account.peerId) |> take(1), @@ -640,6 +392,8 @@ public final class CallController: ViewController { override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + self.isDismissed = false + if !self.didPlayPresentationAnimation { self.didPlayPresentationAnimation = true @@ -648,7 +402,9 @@ public final class CallController: ViewController { self.idleTimerExtensionDisposable.set(self.sharedContext.applicationBindings.pushIdleTimerExtension()) - self.onViewDidAppear?() + DispatchQueue.main.async { [weak self] in + self?.onViewDidAppear?() + } } override public func viewDidDisappear(_ animated: Bool) { @@ -656,7 +412,36 @@ public final class CallController: ViewController { self.idleTimerExtensionDisposable.set(nil) - self.onViewDidDisappear?() + self.notifyDismissed() + } + + func notifyDismissed() { + if !self.isDismissed { + self.isDismissed = true + DispatchQueue.main.async { + self.onViewDidDisappear?() + } + } + } + + final class AnimateOutToGroupChat { + let incomingPeerId: EnginePeer.Id + let incomingVideoLayer: CALayer? + let incomingVideoPlaceholder: VideoSource.Output? + + init( + incomingPeerId: EnginePeer.Id, + incomingVideoLayer: CALayer?, + incomingVideoPlaceholder: VideoSource.Output? + ) { + self.incomingPeerId = incomingPeerId + self.incomingVideoLayer = incomingVideoLayer + self.incomingVideoPlaceholder = incomingVideoPlaceholder + } + } + + func animateOutToGroupChat(completion: @escaping () -> Void) -> AnimateOutToGroupChat? { + return (self.controllerNode as? CallControllerNodeV2)?.animateOutToGroupChat(completion: completion) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -666,41 +451,88 @@ public final class CallController: ViewController { } override public func dismiss(completion: (() -> Void)? = nil) { - self.controllerNode.animateOut(completion: { [weak self] in - self?.didPlayPresentationAnimation = false - self?.presentingViewController?.dismiss(animated: false, completion: nil) + if !self.isAnimatingDismiss { + self.notifyDismissed() - completion?() - }) + self.isAnimatingDismiss = true + self.controllerNode.animateOut(completion: { [weak self] in + guard let self else { + return + } + self.isAnimatingDismiss = false + self.superDismiss() + completion?() + }) + } + } + + public func dismissWithoutAnimation() { + self.superDismiss() + } + + private func superDismiss() { + self.didPlayPresentationAnimation = false + if self.navigationPresentation == .flatModal { + super.dismiss() + } else { + self.presentingViewController?.dismiss(animated: false, completion: nil) + } } private func conferenceAddParticipant() { - let controller = self.call.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( + //TODO:localize + let context = self.call.context + let callPeerId = self.call.peerId + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) + let controller = self.call.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams( context: self.call.context, - filter: [.onlyWriteable], - hasChatListSelector: true, - hasContactSelector: true, - hasGlobalSearch: true, - title: "Add Participant", - pretendPresentedInModal: false + updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), + mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false), + isPeerEnabled: { peer in + guard case let .user(user) = peer else { + return false + } + if user.id == context.account.peerId || user.id == callPeerId { + return false + } + if user.botInfo != nil { + return false + } + return true + } )) - controller.peerSelected = { [weak self, weak controller] peer, _ in - controller?.dismiss() - + controller.navigationPresentation = .modal + let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in guard let self else { + controller?.dismiss() return } - guard case let .call(call) = self.call else { + guard case let .result(peerIds, _) = result else { + controller?.dismiss() return } - guard let call = call as? PresentationCallImpl else { + if peerIds.isEmpty { + controller?.dismiss() return } - let _ = call.requestAddToConference(peerId: peer.id) - } - self.dismiss() + + controller?.displayProgress = true + let _ = self.call.upgradeToConference(completion: { [weak self] _ in + guard let self else { + return + } + + for peerId in peerIds { + if case let .peer(peerId) = peerId { + let _ = (self.call as? PresentationCallImpl)?.requestAddToConference(peerId: peerId) + } + } + + controller?.dismiss() + }) + }) - (self.call.context.sharedContext.mainWindow?.viewController as? NavigationController)?.pushViewController(controller) + self.push(controller) } @objc private func backPressed() { diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index eb1d460a5d..7fd1cdc2dd 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -32,7 +32,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private let account: Account private let presentationData: PresentationData private let statusBar: StatusBar - private let call: CallController.Call + private let call: PresentationCall private let containerView: UIView private let callScreen: PrivateCallScreen @@ -57,6 +57,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP var presentCallRating: ((CallId, Bool) -> Void)? var present: ((ViewController) -> Void)? var callEnded: ((Bool) -> Void)? + var willBeDismissedInteractively: (() -> Void)? var dismissedInteractively: (() -> Void)? var dismissAllTooltips: (() -> Void)? var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? @@ -91,7 +92,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, easyDebugAccess: Bool, - call: CallController.Call + call: PresentationCall ) { self.sharedContext = sharedContext self.account = account @@ -131,13 +132,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } - #if DEBUG - if self.sharedContext.immediateExperimentalUISettings.conferenceCalls { - self.conferenceAddParticipant?() - return - } - #endif - self.call.toggleIsMuted() } self.callScreen.endCallAction = { [weak self] in @@ -185,7 +179,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false, - isEnergySavingEnabled: !self.sharedContext.energyUsageSettings.fullTranslucency + isEnergySavingEnabled: !self.sharedContext.energyUsageSettings.fullTranslucency, + isConferencePossible: self.sharedContext.immediateExperimentalUISettings.conferenceCalls ) self.isMicrophoneMutedDisposable = (call.isMuted @@ -321,11 +316,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP case .active: switch callState.videoState { case .active(let isScreencast, _), .paused(let isScreencast, _): - if isScreencast { - self.call.disableScreencast() - } else { - self.call.disableVideo() - } + let _ = isScreencast + self.call.disableVideo() default: DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: self.presentationData, present: { [weak self] c, a in if let strongSelf = self { @@ -501,22 +493,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.remoteVideo = nil default: switch callState.videoState { - case .active(let isScreencast, let endpointId), .paused(let isScreencast, let endpointId): + case .active(let isScreencast, _), .paused(let isScreencast, _): if isScreencast { self.localVideo = nil } else { if self.localVideo == nil { - switch self.call { - case let .call(call): - if let call = call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: false) { - self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - case let .groupCall(groupCall): - if let groupCall = groupCall as? PresentationGroupCallImpl { - if let videoStreamSignal = groupCall.video(endpointId: endpointId) { - self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - } + if let call = self.call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: false) { + self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) } } } @@ -525,19 +508,10 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } switch callState.remoteVideoState { - case .active(let endpointId), .paused(let endpointId): + case .active, .paused: if self.remoteVideo == nil { - switch self.call { - case let .call(call): - if let call = call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: true) { - self.remoteVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - case let .groupCall(groupCall): - if let groupCall = groupCall as? PresentationGroupCallImpl { - if let videoStreamSignal = groupCall.video(endpointId: endpointId) { - self.remoteVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - } + if let call = self.call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: true) { + self.remoteVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) } } case .inactive: @@ -710,6 +684,17 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } } + func animateOutToGroupChat(completion: @escaping () -> Void) -> CallController.AnimateOutToGroupChat { + self.callScreen.animateOutToGroupChat(completion: completion) + + let takenIncomingVideoLayer = self.callScreen.takeIncomingVideoLayer() + return CallController.AnimateOutToGroupChat( + incomingPeerId: self.call.peerId, + incomingVideoLayer: takenIncomingVideoLayer?.0, + incomingVideoPlaceholder: takenIncomingVideoLayer?.1 + ) + } + func expandFromPipIfPossible() { } @@ -732,6 +717,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 { self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0) self.notifyDismissedInteractivelyOnPanGestureApply = true + self.willBeDismissedInteractively?() self.callScreen.beginPictureInPictureIfPossible() } diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 95003c0a57..04a82610a5 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -194,7 +194,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { private let audioLevelDisposable = MetaDisposable() private let stateDisposable = MetaDisposable() - private var didSetupData = false + private weak var didSetupDataForCall: AnyObject? private var currentSize: CGSize? private var currentContent: Content? @@ -277,8 +277,16 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { let wasEmpty = (self.titleNode.attributedText?.string ?? "").isEmpty - if !self.didSetupData { - self.didSetupData = true + let setupDataForCall: AnyObject? + switch content { + case let .call(_, _, call): + setupDataForCall = call + case let .groupCall(_, _, call): + setupDataForCall = call + } + + if self.didSetupDataForCall !== setupDataForCall { + self.didSetupDataForCall = setupDataForCall switch content { case let .call(sharedContext, account, call): self.presentationData = sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift b/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift index 894fcf94d9..ceb75af373 100644 --- a/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift @@ -63,6 +63,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol var back: (() -> Void)? var presentCallRating: ((CallId, Bool) -> Void)? var callEnded: ((Bool) -> Void)? + var willBeDismissedInteractively: (() -> Void)? var dismissedInteractively: (() -> Void)? var present: ((ViewController) -> Void)? var dismissAllTooltips: (() -> Void)? diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 8eea2892dd..ad8ac4eeca 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -131,20 +131,33 @@ public final class PresentationCallImpl: PresentationCall { private let screencastAudioDataDisposable = MetaDisposable() private let screencastStateDisposable = MetaDisposable() - private var conferenceCall: PresentationGroupCallImpl? + private var conferenceCallImpl: PresentationGroupCallImpl? + public var conferenceCall: PresentationGroupCall? { + if !self.hasConferenceValue { + return nil + } + + return self.conferenceCallImpl + } private var conferenceCallDisposable: Disposable? + private var upgradedToConferenceCompletions = Bag<(PresentationGroupCall) -> Void>() + + private var waitForConferenceCallReadyDisposable: Disposable? + private let hasConferencePromise = ValuePromise(false) + private var hasConferenceValue: Bool = false { + didSet { + if self.hasConferenceValue != oldValue { + self.hasConferencePromise.set(self.hasConferenceValue) + } + } + } + public var hasConference: Signal { + return self.hasConferencePromise.get() + } private var localVideoEndpointId: String? private var remoteVideoEndpointId: String? - private var conferenceSignalingDataDisposable: Disposable? - private var conferenceIsConnected: Bool = false - private var notifyConferenceIsConnectedTimer: Foundation.Timer? - - private var remoteConferenceIsConnectedTimestamp: Double? - private let remoteConferenceIsConnected = ValuePromise(false, ignoreRepeated: true) - private var remoteConferenceIsConnectedTimer: Foundation.Timer? - init( context: AccountContext, audioSession: ManagedAudioSession, @@ -340,9 +353,7 @@ public final class PresentationCallImpl: PresentationCall { self.ongoingContextStateDisposable?.dispose() self.ongoingContextIsFailedDisposable?.dispose() self.ongoingContextIsDroppedDisposable?.dispose() - self.notifyConferenceIsConnectedTimer?.invalidate() - self.conferenceSignalingDataDisposable?.dispose() - self.remoteConferenceIsConnectedTimer?.invalidate() + self.waitForConferenceCallReadyDisposable?.dispose() if let dropCallKitCallTimer = self.dropCallKitCallTimer { dropCallKitCallTimer.invalidate() @@ -540,13 +551,13 @@ public final class PresentationCallImpl: PresentationCall { self.callWasActive = true var isConference = false - if case let .active(_, _, _, _, _, version, _, _, _) = sessionState.state { - isConference = version == "13.0.0" + if case let .active(_, _, _, _, _, _, _, _, conferenceCall) = sessionState.state { + isConference = conferenceCall != nil } else if case .switchedToConference = sessionState.state { isConference = true } - if let callContextState = callContextState { + if let callContextState = callContextState, !isConference { switch callContextState.state { case .initializing: presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) @@ -593,221 +604,290 @@ public final class PresentationCallImpl: PresentationCall { if let (key, keyVisualHash, conferenceCall) = conferenceCallData { if self.conferenceCallDisposable == nil { - presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + self.conferenceCallDisposable = EmptyDisposable + + self.ongoingContextStateDisposable?.dispose() + self.ongoingContextStateDisposable = nil + self.ongoingContext?.stop(debugLogValue: Promise()) + self.ongoingContext = nil - self.conferenceCallDisposable = (self.context.engine.calls.getCurrentGroupCall(callId: conferenceCall.id, accessHash: conferenceCall.accessHash) - |> delay(sessionState.isOutgoing ? 0.0 : 2.0, queue: .mainQueue()) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let self, let result else { + let conferenceCall = PresentationGroupCallImpl( + accountContext: self.context, + audioSession: self.audioSession, + callKitIntegration: self.callKitIntegration, + getDeviceAccessData: self.getDeviceAccessData, + initialCall: EngineGroupCallDescription( + id: conferenceCall.id, + accessHash: conferenceCall.accessHash, + title: nil, + scheduleTimestamp: nil, + subscribedToScheduled: false, + isStream: false + ), + internalId: CallSessionInternalId(), + peerId: nil, + isChannel: false, + invite: nil, + joinAsPeerId: nil, + isStream: false, + encryptionKey: (key, 1), + conferenceFromCallId: conferenceFromCallId, + isConference: true, + sharedAudioDevice: self.sharedAudioDevice + ) + self.conferenceCallImpl = conferenceCall + + conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted) + if let videoCapturer = self.videoCapturer { + conferenceCall.requestVideo(capturer: videoCapturer) + } + + let accountPeerId = conferenceCall.account.peerId + let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = conferenceCall.members + |> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in + guard let members else { + return (nil, nil) + } + var local: String? + var remote: PresentationGroupCallRequestedVideo? + for participant in members.participants { + if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) { + if participant.peer.id == accountPeerId { + local = video.endpointId + } else { + if remote == nil { + remote = video + } + } + } + } + return (local, remote) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) + + var startTimestamp: Double? + self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(), + conferenceCall.state, + videoEndpoints, + conferenceCall.signalBars, + conferenceCall.isFailed + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints, signalBars, isFailed in + guard let self else { return } - let conferenceCall = PresentationGroupCallImpl( - accountContext: self.context, - audioSession: self.audioSession, - callKitIntegration: self.callKitIntegration, - getDeviceAccessData: self.getDeviceAccessData, - initialCall: EngineGroupCallDescription( - id: result.info.id, - accessHash: result.info.accessHash, - title: nil, - scheduleTimestamp: nil, - subscribedToScheduled: false, - isStream: false - ), - internalId: CallSessionInternalId(), - peerId: nil, - isChannel: false, - invite: nil, - joinAsPeerId: nil, - isStream: false, - encryptionKey: (key, 1), - conferenceFromCallId: conferenceFromCallId, - isConference: true, - sharedAudioDevice: self.sharedAudioDevice - ) - self.conferenceCall = conferenceCall + var mappedLocalVideoState: PresentationCallState.VideoState = .inactive + var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive - conferenceCall.setIsMuted(action: .muted(isPushToTalkActive: !self.isMutedValue)) + if let local = videoEndpoints.local { + mappedLocalVideoState = .active(isScreencast: false, endpointId: local) + } + if let remote = videoEndpoints.remote { + mappedRemoteVideoState = .active(endpointId: remote.endpointId) + } - let accountPeerId = conferenceCall.account.peerId - let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = conferenceCall.members - |> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in - guard let members else { - return (nil, nil) + self.localVideoEndpointId = videoEndpoints.local + self.remoteVideoEndpointId = videoEndpoints.remote?.endpointId + + if let conferenceCall = self.conferenceCall { + var requestedVideo: [PresentationGroupCallRequestedVideo] = [] + if let remote = videoEndpoints.remote { + requestedVideo.append(remote) } - var local: String? - var remote: PresentationGroupCallRequestedVideo? - for participant in members.participants { - if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) { - if participant.peer.id == accountPeerId { - local = video.endpointId - } else { - if remote == nil { - remote = video + conferenceCall.setRequestedVideoList(items: requestedVideo) + } + + let mappedState: PresentationCallState.State + if isFailed { + mappedState = .terminating(.error(.disconnected)) + } else { + switch callState.networkState { + case .connecting: + mappedState = .connecting(keyVisualHash) + case .connected: + let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent() + startTimestamp = timestamp + mappedState = .active(timestamp, signalBars, keyVisualHash) + } + } + + if !self.didDropCall && !self.droppedCall { + /*let presentationState = PresentationCallState( + state: mappedState, + videoState: mappedLocalVideoState, + remoteVideoState: mappedRemoteVideoState, + remoteAudioState: .active, + remoteBatteryLevel: .normal + )*/ + let _ = mappedState + + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + + mappedLocalVideoState = .inactive + mappedRemoteVideoState = .inactive + if self.videoCapturer != nil { + mappedLocalVideoState = .active(isScreencast: false, endpointId: "local") + } + + if let callContextState = self.callContextState { + switch callContextState.remoteVideoState { + case .active, .paused: + mappedRemoteVideoState = .active(endpointId: "temp-\(self.peerId.toInt64())") + case .inactive: + break + } + } + + let presentationState = PresentationCallState( + state: .active(timestamp, signalBars, keyVisualHash), + videoState: mappedLocalVideoState, + remoteVideoState: mappedRemoteVideoState, + remoteAudioState: .active, + remoteBatteryLevel: .normal + ) + self.statePromise.set(presentationState) + self.updateTone(presentationState, callContextState: nil, previous: nil) + } + }) + + self.ongoingContextIsFailedDisposable = (conferenceCall.isFailed + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + if !self.didDropCall { + self.didDropCall = true + self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) + } + }) + + self.ongoingContextIsDroppedDisposable = (conferenceCall.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + if !self.didDropCall { + self.didDropCall = true + self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil)) + } + }) + + var audioLevelId: UInt32? + let audioLevel = conferenceCall.audioLevels |> map { audioLevels -> Float in + var result: Float = 0 + for item in audioLevels { + if let audioLevelId { + if item.1 == audioLevelId { + result = item.2 + break + } + } else { + if item.1 != 0 { + audioLevelId = item.1 + result = item.2 + break + } + } + } + + return result + } + + self.audioLevelDisposable = (audioLevel + |> deliverOnMainQueue).start(next: { [weak self] level in + if let strongSelf = self { + strongSelf.audioLevelPromise.set(level) + } + }) + + let waitForLocalVideo = self.videoCapturer != nil + + let waitForRemotePeerId: EnginePeer.Id? = self.peerId + var waitForRemoteVideo: EnginePeer.Id? + if let callContextState = self.callContextState { + switch callContextState.remoteVideoState { + case .active, .paused: + waitForRemoteVideo = self.peerId + case .inactive: + break + } + } + + self.waitForConferenceCallReadyDisposable?.dispose() + self.waitForConferenceCallReadyDisposable = (combineLatest(queue: .mainQueue(), + conferenceCall.state, + conferenceCall.members + ) + |> filter { state, members in + if state.networkState != .connected { + return false + } + if let waitForRemotePeerId { + var found = false + if let members { + for participant in members.participants { + if participant.peer.id == waitForRemotePeerId { + found = true + break + } + } + } + if !found { + return false + } + } + if waitForLocalVideo { + if let members { + for participant in members.participants { + if participant.peer.id == state.myPeerId { + if participant.videoDescription == nil { + return false } } } } - return (local, remote) } - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs == rhs - }) - - let remoteIsConnectedAggregated = combineLatest(queue: .mainQueue(), - self.remoteConferenceIsConnected.get(), - conferenceCall.hasActiveIncomingData - ) - |> map { remoteConferenceIsConnected, hasActiveIncomingData -> Bool in - return remoteConferenceIsConnected || hasActiveIncomingData - } - |> distinctUntilChanged - - var startTimestamp: Double? - self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(), - conferenceCall.state, - videoEndpoints, - conferenceCall.signalBars, - conferenceCall.isFailed, - remoteIsConnectedAggregated - ) - |> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints, signalBars, isFailed, remoteIsConnectedAggregated in - guard let self else { - return - } - - var mappedLocalVideoState: PresentationCallState.VideoState = .inactive - var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive - - if let local = videoEndpoints.local { - mappedLocalVideoState = .active(isScreencast: false, endpointId: local) - } - if let remote = videoEndpoints.remote { - mappedRemoteVideoState = .active(endpointId: remote.endpointId) - } - - self.localVideoEndpointId = videoEndpoints.local - self.remoteVideoEndpointId = videoEndpoints.remote?.endpointId - - if let conferenceCall = self.conferenceCall { - var requestedVideo: [PresentationGroupCallRequestedVideo] = [] - if let remote = videoEndpoints.remote { - requestedVideo.append(remote) - } - conferenceCall.setRequestedVideoList(items: requestedVideo) - } - - var isConnected = false - let mappedState: PresentationCallState.State - if isFailed { - mappedState = .terminating(.error(.disconnected)) - } else { - switch callState.networkState { - case .connecting: - mappedState = .connecting(keyVisualHash) - case .connected: - isConnected = true - if remoteIsConnectedAggregated { - let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent() - startTimestamp = timestamp - mappedState = .active(timestamp, signalBars, keyVisualHash) - } else { - mappedState = .connecting(keyVisualHash) + if let waitForRemoteVideo { + if let members { + for participant in members.participants { + if participant.peer.id == waitForRemoteVideo { + if participant.videoDescription == nil { + return false + } } } } - - self.updateConferenceIsConnected(isConnected: isConnected) - - if !self.didDropCall && !self.droppedCall { - let presentationState = PresentationCallState( - state: mappedState, - videoState: mappedLocalVideoState, - remoteVideoState: mappedRemoteVideoState, - remoteAudioState: .active, - remoteBatteryLevel: .normal - ) - self.statePromise.set(presentationState) - self.updateTone(presentationState, callContextState: nil, previous: nil) - } - }) - - self.ongoingContextIsFailedDisposable = (conferenceCall.isFailed - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] _ in - guard let self else { - return - } - if !self.didDropCall { - self.didDropCall = true - self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) - } - }) - - self.ongoingContextIsDroppedDisposable = (conferenceCall.canBeRemoved - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] _ in - guard let self else { - return - } - if !self.didDropCall { - self.didDropCall = true - self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil)) - } - }) - - var audioLevelId: UInt32? - let audioLevel = conferenceCall.audioLevels |> map { audioLevels -> Float in - var result: Float = 0 - for item in audioLevels { - if let audioLevelId { - if item.1 == audioLevelId { - result = item.2 - break - } - } else { - if item.1 != 0 { - audioLevelId = item.1 - result = item.2 - break - } - } - } - - return result } - - self.audioLevelDisposable = (audioLevel - |> deliverOnMainQueue).start(next: { [weak self] level in - if let strongSelf = self { - strongSelf.audioLevelPromise.set(level) - } - }) - - let localIsConnected = conferenceCall.state - |> map { state -> Bool in - switch state.networkState { - case .connected: - return true - default: - return false - } + return true + } + |> map { _, _ -> Void in + return Void() + } + |> take(1) + |> timeout(10.0, queue: .mainQueue(), alternate: .single(Void()))).start(next: { [weak self] _ in + guard let self else { + return } - |> distinctUntilChanged + self.hasConferenceValue = true - let bothLocalAndRemoteConnected = combineLatest(queue: .mainQueue(), - localIsConnected, - remoteIsConnectedAggregated - ) - |> map { localIsConnected, remoteIsConnectedAggregated -> Bool in - return localIsConnected && remoteIsConnectedAggregated + let upgradedToConferenceCompletions = self.upgradedToConferenceCompletions.copyItems() + self.upgradedToConferenceCompletions.removeAll() + for f in upgradedToConferenceCompletions { + f(conferenceCall) } - |> distinctUntilChanged - - conferenceCall.internal_isRemoteConnected.set(bothLocalAndRemoteConnected) }) } } @@ -817,26 +897,10 @@ public final class PresentationCallImpl: PresentationCall { if let _ = audioSessionControl { self.audioSessionShouldBeActive.set(true) } - case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall): - if conferenceCall == nil, version == "13.0.0" { - self.createConferenceIfPossible() - } - + case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, _): self.audioSessionShouldBeActive.set(true) - if version == "13.0.0" && self.conferenceSignalingDataDisposable == nil { - self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in - Queue.mainQueue().async { - guard let self else { - return - } - - self.processConferenceSignalingData(dataList: dataList) - } - }) - } - - if version == "13.0.0" || conferenceCallData != nil { + if conferenceCallData != nil { if sessionState.isOutgoing { self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) } @@ -910,7 +974,7 @@ public final class PresentationCallImpl: PresentationCall { if wasActive { let debugLogValue = Promise() self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue) - let _ = self.conferenceCall?.leave(terminateIfPossible: false).start() + let _ = self.conferenceCallImpl?.leave(terminateIfPossible: false).start() } case .dropping: break @@ -918,7 +982,7 @@ public final class PresentationCallImpl: PresentationCall { self.audioSessionShouldBeActive.set(false) if wasActive { let debugLogValue = Promise() - if let conferenceCall = self.conferenceCall { + if let conferenceCall = self.conferenceCallImpl { debugLogValue.set(conferenceCall.debugLog.get()) let _ = conferenceCall.leave(terminateIfPossible: false).start() } else { @@ -1064,88 +1128,6 @@ public final class PresentationCallImpl: PresentationCall { } } - private func updateConferenceIsConnected(isConnected: Bool) { - if self.conferenceIsConnected != isConnected { - self.conferenceIsConnected = isConnected - self.sendConferenceIsConnectedState() - } - - if self.notifyConferenceIsConnectedTimer == nil { - self.notifyConferenceIsConnectedTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in - guard let self else { - return - } - self.sendConferenceIsConnectedState() - }) - } - } - - private func sendConferenceIsConnectedState() { - self.sendConferenceSignalingMessage(dict: ["_$": "s", "c": self.conferenceIsConnected]) - } - - private func processConferenceSignalingData(dataList: [Data]) { - for data in dataList { - if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - self.processConferenceSignalingMessage(dict: dict) - } - } - } - - private func processConferenceSignalingMessage(dict: [String: Any]) { - if let type = dict["_$"] as? String { - switch type { - case "s": - let isConnected = dict["c"] as? Bool ?? false - self.remoteConferenceIsConnected.set(isConnected) - - if isConnected { - self.remoteConferenceIsConnectedTimestamp = CFAbsoluteTimeGetCurrent() - } - - if self.remoteConferenceIsConnectedTimer == nil && isConnected { - self.remoteConferenceIsConnectedTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in - guard let self else { - return - } - let timestamp = CFAbsoluteTimeGetCurrent() - if let remoteConferenceIsConnectedTimestamp = self.remoteConferenceIsConnectedTimestamp { - if remoteConferenceIsConnectedTimestamp + 4.0 < timestamp { - self.remoteConferenceIsConnected.set(false) - } - - if remoteConferenceIsConnectedTimestamp + 10.0 < timestamp { - if !self.didDropCall { - self.didDropCall = true - - let presentationState = PresentationCallState( - state: .terminating(.error(.disconnected)), - videoState: .inactive, - remoteVideoState: .inactive, - remoteAudioState: .active, - remoteBatteryLevel: .normal - ) - self.statePromise.set(presentationState) - self.updateTone(presentationState, callContextState: nil, previous: nil) - - self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) - } - } - } - }) - } - default: - break - } - } - } - - private func sendConferenceSignalingMessage(dict: [String: Any]) { - if let data = try? JSONSerialization.data(withJSONObject: dict) { - self.context.account.callSessionManager.sendSignalingData(internalId: self.internalId, data: data) - } - } - private func updateIsAudioSessionActive(_ value: Bool) { if self.isAudioSessionActive != value { self.isAudioSessionActive = value @@ -1202,7 +1184,7 @@ public final class PresentationCallImpl: PresentationCall { public func hangUp() -> Signal { let debugLogValue = Promise() self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get()) - if let conferenceCall = self.conferenceCall { + if let conferenceCall = self.conferenceCallImpl { debugLogValue.set(conferenceCall.debugLog.get()) let _ = conferenceCall.leave(terminateIfPossible: false).start() } else { @@ -1215,7 +1197,7 @@ public final class PresentationCallImpl: PresentationCall { public func rejectBusy() { self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil)) let debugLog = Promise() - if let conferenceCall = self.conferenceCall { + if let conferenceCall = self.conferenceCallImpl { debugLog.set(conferenceCall.debugLog.get()) let _ = conferenceCall.leave(terminateIfPossible: false).start() } else { @@ -1231,7 +1213,6 @@ public final class PresentationCallImpl: PresentationCall { self.isMutedValue = value self.isMutedPromise.set(self.isMutedValue) self.ongoingContext?.setIsMuted(self.isMutedValue) - self.conferenceCall?.setIsMuted(action: .muted(isPushToTalkActive: !self.isMutedValue)) } public func requestVideo() { @@ -1242,7 +1223,7 @@ public final class PresentationCallImpl: PresentationCall { if let videoCapturer = self.videoCapturer { if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(videoCapturer) - } else if let conferenceCall = self.conferenceCall { + } else if let conferenceCall = self.conferenceCallImpl { conferenceCall.requestVideo(capturer: videoCapturer) } } @@ -1258,7 +1239,7 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer = nil if let ongoingContext = self.ongoingContext { ongoingContext.disableVideo() - } else if let conferenceCall = self.conferenceCall { + } else if let conferenceCall = self.conferenceCallImpl { conferenceCall.disableVideo() } } @@ -1308,7 +1289,7 @@ public final class PresentationCallImpl: PresentationCall { self.isScreencastActive = true if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(screencastCapturer) - } else if let conferenceCall = self.conferenceCall { + } else if let conferenceCall = self.conferenceCallImpl { conferenceCall.requestVideo(capturer: screencastCapturer) } } @@ -1321,7 +1302,7 @@ public final class PresentationCallImpl: PresentationCall { } self.isScreencastActive = false self.ongoingContext?.disableVideo() - self.conferenceCall?.disableVideo() + self.conferenceCallImpl?.disableVideo() if reset { self.resetScreencastContext() } @@ -1332,6 +1313,25 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer?.setIsVideoEnabled(!isPaused) } + public func upgradeToConference(completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { + if let conferenceCall = self.conferenceCall { + completion(conferenceCall) + return EmptyDisposable + } + + let index = self.upgradedToConferenceCompletions.add(completion) + self.callSessionManager.createConferenceIfNecessary(internalId: self.internalId) + + return ActionDisposable { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + self.upgradedToConferenceCompletions.remove(index) + } + } + } + public func requestAddToConference(peerId: EnginePeer.Id) -> Disposable { var conferenceCall: (conference: GroupCallReference, encryptionKey: Data)? if let sessionState = self.sessionState { @@ -1388,7 +1388,7 @@ public final class PresentationCallImpl: PresentationCall { if isIncoming { if let ongoingContext = self.ongoingContext { return ongoingContext.video(isIncoming: isIncoming) - } else if let conferenceCall = self.conferenceCall, let remoteVideoEndpointId = self.remoteVideoEndpointId { + } else if let conferenceCall = self.conferenceCallImpl, let remoteVideoEndpointId = self.remoteVideoEndpointId { return conferenceCall.video(endpointId: remoteVideoEndpointId) } else { return nil @@ -1400,10 +1400,6 @@ public final class PresentationCallImpl: PresentationCall { } } - public func createConferenceIfPossible() { - self.callSessionManager.createConferenceIfNecessary(internalId: self.internalId) - } - public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) { if self.videoCapturer == nil { let videoCapturer = OngoingCallVideoCapturer() diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 4ef10924e5..74d10bbcf9 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1887,6 +1887,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels) self.connectPendingVideoSubscribers() + if let videoCapturer = self.videoCapturer { + genericCallContext.requestVideo(videoCapturer) + } + if case let .call(callContext) = genericCallContext { var lastTimestamp: Double? self.hasActiveIncomingDataDisposable?.dispose() @@ -2336,6 +2340,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { peerView = .single(nil) } + self.updateLocalVideoState() + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), participantsContext.state, participantsContext.activeSpeakers, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index c3eea4b4eb..fd36252698 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -191,6 +191,7 @@ final class VideoChatParticipantVideoComponent: Component { private let pinchContainerNode: PinchSourceContainerNode private let extractedContainerView: ContextExtractedContentContainingView private var videoSource: AdaptedCallVideoSource? + private var videoPlaceholder: VideoSource.Output? private var videoDisposable: Disposable? private var videoBackgroundLayer: SimpleLayer? private var videoLayer: PrivateCallVideoLayer? @@ -263,6 +264,11 @@ final class VideoChatParticipantVideoComponent: Component { } } + func updatePlaceholder(placeholder: VideoSource.Output) { + self.videoPlaceholder = placeholder + self.componentState?.updated(transition: .immediate, isLocal: true) + } + func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -456,6 +462,46 @@ final class VideoChatParticipantVideoComponent: Component { videoBackgroundLayer.isHidden = true } + let videoUpdated: () -> Void = { [weak self] in + guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else { + return + } + + var videoOutput = videoSource.currentOutput + var isPlaceholder = false + if videoOutput == nil { + isPlaceholder = true + videoOutput = self.videoPlaceholder + } else { + self.videoPlaceholder = nil + } + + videoLayer.video = videoOutput + + if let videoOutput { + let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) + if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { + self.awaitingFirstVideoFrameForUnpause = false + + self.videoSpec = videoSpec + if !self.isUpdating { + var transition: ComponentTransition = .immediate + if !isPlaceholder { + transition = transition.withUserData(AnimationHint(kind: .videoAvailabilityChanged)) + } + self.componentState?.updated(transition: transition, isLocal: true) + } + } + } else { + if self.videoSpec != nil { + self.videoSpec = nil + if !self.isUpdating { + self.componentState?.updated(transition: .immediate, isLocal: true) + } + } + } + } + let videoLayer: PrivateCallVideoLayer if let current = self.videoLayer { videoLayer = current @@ -473,36 +519,16 @@ final class VideoChatParticipantVideoComponent: Component { self.videoSource = videoSource self.videoDisposable?.dispose() - self.videoDisposable = videoSource.addOnUpdated { [weak self] in - guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else { - return - } - - let videoOutput = videoSource.currentOutput - videoLayer.video = videoOutput - - if let videoOutput { - let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) - if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { - self.awaitingFirstVideoFrameForUnpause = false - - self.videoSpec = videoSpec - if !self.isUpdating { - self.componentState?.updated(transition: ComponentTransition.immediate.withUserData(AnimationHint(kind: .videoAvailabilityChanged)), isLocal: true) - } - } - } else { - if self.videoSpec != nil { - self.videoSpec = nil - if !self.isUpdating { - self.componentState?.updated(transition: .immediate, isLocal: true) - } - } - } + self.videoDisposable = videoSource.addOnUpdated { + videoUpdated() } } } + if let _ = self.videoPlaceholder, videoLayer.video == nil { + videoUpdated() + } + transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) if let videoSpec = self.videoSpec { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 7733798179..85cf39dffc 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -11,6 +11,7 @@ import MultilineTextComponent import TelegramPresentationData import PeerListItemComponent import ContextUI +import CallScreen final class VideoChatParticipantsComponent: Component { struct Layout: Equatable { @@ -1616,6 +1617,27 @@ final class VideoChatParticipantsComponent: Component { } } + func itemFrame(peerId: EnginePeer.Id, isPresentation: Bool) -> CGRect? { + for (key, itemView) in self.gridItemViews { + if key.id == peerId && key.isPresentation == isPresentation { + if let itemComponentView = itemView.view.view { + return itemComponentView.convert(itemComponentView.bounds, to: self) + } + } + } + return nil + } + + func updateItemPlaceholder(peerId: EnginePeer.Id, isPresentation: Bool, placeholder: VideoSource.Output) { + for (key, itemView) in self.gridItemViews { + if key.id == peerId && key.isPresentation == isPresentation { + if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View { + itemComponentView.updatePlaceholder(placeholder: placeholder) + } + } + } + } + func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -1854,7 +1876,7 @@ final class VideoChatParticipantsComponent: Component { return UIColor(white: 1.0, alpha: 1.0) } else { let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) - let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + let value: CGFloat = 1.0 - Display.bezierPoint(0.42, 0.0, 0.58, 1.0, step) return UIColor(white: 0.0, alpha: baseGradientAlpha * value) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index a50b442a57..70ebfc1ef5 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -114,6 +114,8 @@ final class VideoChatScreenComponent: Component { var focusedSpeakerAutoSwitchDeadline: Double = 0.0 var isTwoColumnSidebarHidden: Bool = false + var isAnimatedOutFromPrivateCall: Bool = false + let inviteDisposable = MetaDisposable() let currentAvatarMixin = Atomic(value: nil) let updateAvatarDisposable = MetaDisposable() @@ -164,6 +166,76 @@ final class VideoChatScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.5)) } + func animateIn(sourceCallController: CallController) { + let sourceCallControllerView = sourceCallController.view + var isAnimationFinished = false + let animateOutData = sourceCallController.animateOutToGroupChat(completion: { [weak sourceCallControllerView] in + isAnimationFinished = true + sourceCallControllerView?.removeFromSuperview() + }) + + var expandedPeer: (id: EnginePeer.Id, isPresentation: Bool)? + if let animateOutData, animateOutData.incomingVideoLayer != nil { + if let members = self.members, let participant = members.participants.first(where: { $0.peer.id == animateOutData.incomingPeerId }) { + if let _ = participant.videoDescription { + expandedPeer = (participant.peer.id, false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true) + } + } + } + + self.isAnimatedOutFromPrivateCall = true + self.verticalPanState = nil + + self.state?.updated(transition: .immediate) + + if !isAnimationFinished { + if let participantsView = self.participants.view { + self.containerView.insertSubview(sourceCallController.view, belowSubview: participantsView) + } else { + self.containerView.addSubview(sourceCallController.view) + } + } + + let transition: ComponentTransition = .spring(duration: 0.4) + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.25) + + self.isAnimatedOutFromPrivateCall = false + self.expandedParticipantsVideoState = nil + self.state?.updated(transition: transition) + + if let animateOutData, let expandedPeer, let incomingVideoLayer = animateOutData.incomingVideoLayer, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View, let targetFrame = participantsView.itemFrame(peerId: expandedPeer.id, isPresentation: expandedPeer.isPresentation) { + if let incomingVideoPlaceholder = animateOutData.incomingVideoPlaceholder { + participantsView.updateItemPlaceholder(peerId: expandedPeer.id, isPresentation: expandedPeer.isPresentation, placeholder: incomingVideoPlaceholder) + } + + let incomingVideoLayerFrame = incomingVideoLayer.convert(incomingVideoLayer.frame, to: sourceCallControllerView?.layer) + + let targetContainer = SimpleLayer() + targetContainer.masksToBounds = true + targetContainer.backgroundColor = UIColor.blue.cgColor + targetContainer.cornerRadius = 10.0 + + self.containerView.layer.insertSublayer(targetContainer, above: participantsView.layer) + + targetContainer.frame = incomingVideoLayerFrame + + targetContainer.addSublayer(incomingVideoLayer) + incomingVideoLayer.position = CGRect(origin: CGPoint(), size: incomingVideoLayerFrame.size).center + let sourceFitScale = max(incomingVideoLayerFrame.width / incomingVideoLayerFrame.width, incomingVideoLayerFrame.height / incomingVideoLayerFrame.height) + incomingVideoLayer.transform = CATransform3DMakeScale(sourceFitScale, sourceFitScale, 1.0) + + let targetFrame = participantsView.convert(targetFrame, to: self) + let targetFitScale = min(incomingVideoLayerFrame.width / targetFrame.width, incomingVideoLayerFrame.height / targetFrame.height) + + transition.setFrame(layer: targetContainer, frame: targetFrame, completion: { [weak targetContainer] _ in + targetContainer?.removeFromSuperlayer() + }) + transition.setTransform(layer: incomingVideoLayer, transform: CATransform3DMakeScale(targetFitScale, targetFitScale, 1.0)) + alphaTransition.setAlpha(layer: targetContainer, alpha: 0.0) + } + } + func animateOut(completion: @escaping () -> Void) { self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.completionOnPanGestureApply = completion @@ -1027,30 +1099,32 @@ final class VideoChatScreenComponent: Component { self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) }) - self.memberEventsDisposable = (component.call.memberEvents - |> deliverOnMainQueue).start(next: { [weak self] event in - guard let self, let members = self.members, let component = self.component, let environment = self.environment else { - return - } - if event.joined { - var displayEvent = false - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - displayEvent = false + if component.call.peerId != nil { + self.memberEventsDisposable = (component.call.memberEvents + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let self, let members = self.members, let component = self.component, let environment = self.environment else { + return } - if members.totalCount < 40 { - displayEvent = true - } else if event.peer.isVerified { - displayEvent = true - } else if event.isContact || event.isInChatList { - displayEvent = true + if event.joined { + var displayEvent = false + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + displayEvent = false + } + if members.totalCount < 40 { + displayEvent = true + } else if event.peer.isVerified { + displayEvent = true + } else if event.isContact || event.isInChatList { + displayEvent = true + } + + if displayEvent { + let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } } - - if displayEvent { - let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - } - }) + }) + } } self.isPresentedValue.set(environment.isVisible) @@ -1210,6 +1284,7 @@ final class VideoChatScreenComponent: Component { self.containerView.addSubview(navigationLeftButtonView) } transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + alphaTransition.setAlpha(view: navigationLeftButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) @@ -1218,6 +1293,7 @@ final class VideoChatScreenComponent: Component { self.containerView.addSubview(navigationRightButtonView) } transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame) + alphaTransition.setAlpha(view: navigationRightButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } if isTwoColumnLayout { @@ -1300,10 +1376,11 @@ final class VideoChatScreenComponent: Component { maxTitleWidth -= 110.0 } + //TODO:localize let titleSize = self.title.update( transition: transition, component: AnyComponent(VideoChatTitleComponent( - title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? " ", + title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? "Group Call", status: idleTitleStatusText, isRecording: self.callState?.recordingStartTimestamp != nil, strings: environment.strings, @@ -1350,6 +1427,7 @@ final class VideoChatScreenComponent: Component { self.containerView.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) + alphaTransition.setAlpha(view: titleView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } let areButtonsCollapsed: Bool @@ -1411,6 +1489,10 @@ final class VideoChatScreenComponent: Component { let actionMicrophoneButtonSpacing = min(effectiveMaxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter)) + if self.isAnimatedOutFromPrivateCall { + collapsedMicrophoneButtonFrame.origin.y = availableSize.height + 48.0 + } + var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter)) var isMainColumnHidden = false @@ -1617,6 +1699,9 @@ final class VideoChatScreenComponent: Component { if let callState = self.callState, callState.scheduleTimestamp != nil { participantsAlpha = 0.0 } + if self.isAnimatedOutFromPrivateCall { + participantsAlpha = 0.0 + } alphaTransition.setAlpha(view: participantsView, alpha: participantsAlpha) } @@ -1919,12 +2004,16 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo private var isAnimatingDismiss: Bool = false private var idleTimerExtensionDisposable: Disposable? + + private var sourceCallController: CallController? public init( initialData: InitialData, - call: PresentationGroupCall + call: PresentationGroupCall, + sourceCallController: CallController? ) { self.call = call + self.sourceCallController = sourceCallController let theme = customizeDefaultDarkPresentationTheme( theme: defaultDarkPresentationTheme, @@ -1964,7 +2053,12 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo self.isDismissed = false if let componentView = self.node.hostView.componentView as? VideoChatScreenComponent.View { - componentView.animateIn() + if let sourceCallController = self.sourceCallController { + self.sourceCallController = nil + componentView.animateIn(sourceCallController: sourceCallController) + } else { + componentView.animateIn() + } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index 4bbd537ed0..fb4d9b614d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -23,9 +23,6 @@ extension VideoChatScreenComponent.View { guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { return } - guard let peer = self.peer else { - return - } guard let callState = self.callState else { return } @@ -34,7 +31,7 @@ extension VideoChatScreenComponent.View { var items: [ContextMenuItem] = [] - if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { + if self.peer != nil, let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { for peer in displayAsPeers { if peer.peer.id == callState.myPeerId { let avatarSize = CGSize(width: 28.0, height: 28.0) @@ -98,7 +95,7 @@ extension VideoChatScreenComponent.View { }))) var hasPermissions = true - if case let .channel(chatPeer) = peer { + if let peer = self.peer, case let .channel(chatPeer) = peer { if case .broadcast = chatPeer.info { hasPermissions = false } else if chatPeer.flags.contains(.isGigagroup) { @@ -152,54 +149,56 @@ extension VideoChatScreenComponent.View { } } - let qualityList: [(Int, String)] = [ - (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly), - (180, "180p"), - (360, "360p"), - (Int.max, "720p") - ] - - let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" - items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in - return nil - }, action: { [weak self] c, _ in - guard let self else { - c?.dismiss(completion: nil) - return - } + if let members = self.members, members.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) { + let qualityList: [(Int, String)] = [ + (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly), + (180, "180p"), + (360, "360p"), + (Int.max, "720p") + ] - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - - for (quality, title) in qualityList { - let isSelected = self.maxVideoQuality == quality - items.append(.action(ContextMenuActionItem(text: title, icon: { _ in - if isSelected { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - - if self.maxVideoQuality != quality { - self.maxVideoQuality = quality - self.state?.updated(transition: .immediate) - } + let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" + items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in + return nil + }, action: { [weak self] c, _ in + guard let self else { + c?.dismiss(completion: nil) + return + } + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() }))) - } - - c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) - }))) + items.append(.separator) + + for (quality, title) in qualityList { + let isSelected = self.maxVideoQuality == quality + items.append(.action(ContextMenuActionItem(text: title, icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + if self.maxVideoQuality != quality { + self.maxVideoQuality = quality + self.state?.updated(transition: .immediate) + } + }))) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) + }))) + } if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { if component.call.hasScreencast { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 651b385172..8033762d00 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7148,12 +7148,6 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte } } -public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController { - let useV2 = shouldUseV2VideoChatImpl(context: accountContext) - - if useV2 { - return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call) - } else { - return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call) - } +public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any, sourceCallController: CallController?) -> VoiceChatController { + return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call, sourceCallController: sourceCallController) } diff --git a/submodules/TelegramCore/BUILD b/submodules/TelegramCore/BUILD index 2d8a1a61bc..2b09a36713 100644 --- a/submodules/TelegramCore/BUILD +++ b/submodules/TelegramCore/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/Utils/RangeSet:RangeSet", "//submodules/Utils/DarwinDirStat", "//submodules/Emoji", + "//submodules/TelegramCore/FlatSerialization", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCore/FlatBuffers/BUILD b/submodules/TelegramCore/FlatBuffers/BUILD new file mode 100644 index 0000000000..e2d518aa50 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/BUILD @@ -0,0 +1,16 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "FlatBuffers", + module_name = "FlatBuffers", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramCore/FlatBuffers/Sources/ByteBuffer.swift b/submodules/TelegramCore/FlatBuffers/Sources/ByteBuffer.swift new file mode 100644 index 0000000000..9442c855a6 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/ByteBuffer.swift @@ -0,0 +1,542 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// `ByteBuffer` is the interface that stores the data for a `Flatbuffers` object +/// it allows users to write and read data directly from memory thus the use of its +/// functions should be used +@frozen +public struct ByteBuffer { + + /// Storage is a container that would hold the memory pointer to solve the issue of + /// deallocating the memory that was held by (memory: UnsafeMutableRawPointer) + @usableFromInline + final class Storage { + // This storage doesn't own the memory, therefore, we won't deallocate on deinit. + private let unowned: Bool + /// pointer to the start of the buffer object in memory + var memory: UnsafeMutableRawPointer + /// Capacity of UInt8 the buffer can hold + var capacity: Int + + @usableFromInline + init(count: Int, alignment: Int) { + memory = UnsafeMutableRawPointer.allocate( + byteCount: count, + alignment: alignment) + capacity = count + unowned = false + } + + @usableFromInline + init(memory: UnsafeMutableRawPointer, capacity: Int, unowned: Bool) { + self.memory = memory + self.capacity = capacity + self.unowned = unowned + } + + deinit { + if !unowned { + memory.deallocate() + } + } + + @usableFromInline + func copy(from ptr: UnsafeRawPointer, count: Int) { + assert( + !unowned, + "copy should NOT be called on a buffer that is built by assumingMemoryBound") + memory.copyMemory(from: ptr, byteCount: count) + } + + @usableFromInline + func initialize(for size: Int) { + assert( + !unowned, + "initalize should NOT be called on a buffer that is built by assumingMemoryBound") + memset(memory, 0, size) + } + + /// Reallocates the buffer incase the object to be written doesnt fit in the current buffer + /// - Parameter size: Size of the current object + @usableFromInline + func reallocate(_ size: Int, writerSize: Int, alignment: Int) { + let currentWritingIndex = capacity &- writerSize + while capacity <= writerSize &+ size { + capacity = capacity << 1 + } + + /// solution take from Apple-NIO + capacity = capacity.convertToPowerofTwo + + let newData = UnsafeMutableRawPointer.allocate( + byteCount: capacity, + alignment: alignment) + memset(newData, 0, capacity &- writerSize) + memcpy( + newData.advanced(by: capacity &- writerSize), + memory.advanced(by: currentWritingIndex), + writerSize) + memory.deallocate() + memory = newData + } + } + + @usableFromInline var _storage: Storage + + /// The size of the elements written to the buffer + their paddings + private var _writerSize: Int = 0 + /// Alignment of the current memory being written to the buffer + var alignment = 1 + /// Current Index which is being used to write to the buffer, it is written from the end to the start of the buffer + var writerIndex: Int { _storage.capacity &- _writerSize } + + /// Reader is the position of the current Writer Index (capacity - size) + public var reader: Int { writerIndex } + /// Current size of the buffer + public var size: UOffset { UOffset(_writerSize) } + /// Public Pointer to the buffer object in memory. This should NOT be modified for any reason + public var memory: UnsafeMutableRawPointer { _storage.memory } + /// Current capacity for the buffer + public var capacity: Int { _storage.capacity } + /// Crash if the trying to read an unaligned buffer instead of allowing users to read them. + public let allowReadingUnalignedBuffers: Bool + + /// Constructor that creates a Flatbuffer object from a UInt8 + /// - Parameter + /// - bytes: Array of UInt8 + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + bytes: [UInt8], + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + var b = bytes + _storage = Storage(count: bytes.count, alignment: alignment) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + b.withUnsafeMutableBytes { bufferPointer in + _storage.copy(from: bufferPointer.baseAddress!, count: bytes.count) + } + } + + #if !os(WASI) + /// Constructor that creates a Flatbuffer from the Swift Data type object + /// - Parameter + /// - data: Swift data Object + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + data: Data, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + var b = data + _storage = Storage(count: data.count, alignment: alignment) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + b.withUnsafeMutableBytes { bufferPointer in + _storage.copy(from: bufferPointer.baseAddress!, count: data.count) + } + } + #endif + + /// Constructor that creates a Flatbuffer instance with a size + /// - Parameter: + /// - size: Length of the buffer + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + init(initialSize size: Int) { + let size = size.convertToPowerofTwo + _storage = Storage(count: size, alignment: alignment) + _storage.initialize(for: size) + allowReadingUnalignedBuffers = false + } + + #if swift(>=5.0) && !os(WASI) + /// Constructor that creates a Flatbuffer object from a ContiguousBytes + /// - Parameters: + /// - contiguousBytes: Binary stripe to use as the buffer + /// - count: amount of readable bytes + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + contiguousBytes: Bytes, + count: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(count: count, alignment: alignment) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + contiguousBytes.withUnsafeBytes { buf in + _storage.copy(from: buf.baseAddress!, count: buf.count) + } + } + #endif + + /// Constructor that creates a Flatbuffer from unsafe memory region without copying + /// - Parameter: + /// - assumingMemoryBound: The unsafe memory region + /// - capacity: The size of the given memory region + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + assumingMemoryBound memory: UnsafeMutableRawPointer, + capacity: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(memory: memory, capacity: capacity, unowned: true) + _writerSize = capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + } + + /// Creates a copy of the buffer that's being built by calling sizedBuffer + /// - Parameters: + /// - memory: Current memory of the buffer + /// - count: count of bytes + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + init( + memory: UnsafeMutableRawPointer, + count: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(count: count, alignment: alignment) + _storage.copy(from: memory, count: count) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + } + + /// Creates a copy of the existing flatbuffer, by copying it to a different memory. + /// - Parameters: + /// - memory: Current memory of the buffer + /// - count: count of bytes + /// - removeBytes: Removes a number of bytes from the current size + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + init( + memory: UnsafeMutableRawPointer, + count: Int, + removing removeBytes: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(count: count, alignment: alignment) + _storage.copy(from: memory, count: count) + _writerSize = removeBytes + allowReadingUnalignedBuffers = allowUnalignedBuffers + } + + /// Fills the buffer with padding by adding to the writersize + /// - Parameter padding: Amount of padding between two to be serialized objects + @inline(__always) + @usableFromInline + mutating func fill(padding: Int) { + assert(padding >= 0, "Fill should be larger than or equal to zero") + ensureSpace(size: padding) + _writerSize = _writerSize &+ (MemoryLayout.size &* padding) + } + + /// Adds an array of type Scalar to the buffer memory + /// - Parameter elements: An array of Scalars + @inline(__always) + @usableFromInline + mutating func push(elements: [T]) { + elements.withUnsafeBytes { ptr in + ensureSpace(size: ptr.count) + memcpy( + _storage.memory.advanced(by: writerIndex &- ptr.count), + ptr.baseAddress!, + ptr.count) + _writerSize = _writerSize &+ ptr.count + } + } + + /// Adds an array of type Scalar to the buffer memory + /// - Parameter elements: An array of Scalars + @inline(__always) + @usableFromInline + mutating func push(elements: [T]) { + elements.withUnsafeBytes { ptr in + ensureSpace(size: ptr.count) + memcpy( + _storage.memory.advanced(by: writerIndex &- ptr.count), + ptr.baseAddress!, + ptr.count) + _writerSize = _writerSize &+ ptr.count + } + } + + /// Adds a `ContiguousBytes` to buffer memory + /// - Parameter value: bytes to copy + #if swift(>=5.0) && !os(WASI) + @inline(__always) + @usableFromInline + mutating func push(bytes: ContiguousBytes) { + bytes.withUnsafeBytes { ptr in + ensureSpace(size: ptr.count) + memcpy( + _storage.memory.advanced(by: writerIndex &- ptr.count), + ptr.baseAddress!, + ptr.count) + _writerSize = _writerSize &+ ptr.count + } + } + #endif + + /// Adds an object of type NativeStruct into the buffer + /// - Parameters: + /// - value: Object that will be written to the buffer + /// - size: size to subtract from the WriterIndex + @usableFromInline + @inline(__always) + mutating func push(struct value: T, size: Int) { + ensureSpace(size: size) + withUnsafePointer(to: value) { + memcpy( + _storage.memory.advanced(by: writerIndex &- size), + $0, + size) + _writerSize = _writerSize &+ size + } + } + + /// Adds an object of type Scalar into the buffer + /// - Parameters: + /// - value: Object that will be written to the buffer + /// - len: Offset to subtract from the WriterIndex + @inline(__always) + @usableFromInline + mutating func push(value: T, len: Int) { + ensureSpace(size: len) + withUnsafePointer(to: value) { + memcpy( + _storage.memory.advanced(by: writerIndex &- len), + $0, + len) + _writerSize = _writerSize &+ len + } + } + + /// Adds a string to the buffer using swift.utf8 object + /// - Parameter str: String that will be added to the buffer + /// - Parameter len: length of the string + @inline(__always) + @usableFromInline + mutating func push(string str: String, len: Int) { + ensureSpace(size: len) + if str.utf8 + .withContiguousStorageIfAvailable({ self.push(bytes: $0, len: len) }) != + nil + { + } else { + let utf8View = str.utf8 + for c in utf8View.reversed() { + push(value: c, len: 1) + } + } + } + + /// Writes a string to Bytebuffer using UTF8View + /// - Parameters: + /// - bytes: Pointer to the view + /// - len: Size of string + @usableFromInline + @inline(__always) + mutating func push( + bytes: UnsafeBufferPointer, + len: Int) -> Bool + { + memcpy( + _storage.memory.advanced(by: writerIndex &- len), + bytes.baseAddress!, + len) + _writerSize = _writerSize &+ len + return true + } + + /// Write stores an object into the buffer directly or indirectly. + /// + /// Direct: ignores the capacity of buffer which would mean we are referring to the direct point in memory + /// indirect: takes into respect the current capacity of the buffer (capacity - index), writing to the buffer from the end + /// - Parameters: + /// - value: Value that needs to be written to the buffer + /// - index: index to write to + /// - direct: Should take into consideration the capacity of the buffer + @inline(__always) + func write(value: T, index: Int, direct: Bool = false) { + var index = index + if !direct { + index = _storage.capacity &- index + } + assert(index < _storage.capacity, "Write index is out of writing bound") + assert(index >= 0, "Writer index should be above zero") + withUnsafePointer(to: value) { + memcpy( + _storage.memory.advanced(by: index), + $0, + MemoryLayout.size) + } + } + + /// Makes sure that buffer has enouch space for each of the objects that will be written into it + /// - Parameter size: size of object + @discardableResult + @usableFromInline + @inline(__always) + mutating func ensureSpace(size: Int) -> Int { + if size &+ _writerSize > _storage.capacity { + _storage.reallocate(size, writerSize: _writerSize, alignment: alignment) + } + assert(size < FlatBufferMaxSize, "Buffer can't grow beyond 2 Gigabytes") + return size + } + + /// pops the written VTable if it's already written into the buffer + /// - Parameter size: size of the `VTable` + @usableFromInline + @inline(__always) + mutating func pop(_ size: Int) { + assert( + (_writerSize &- size) > 0, + "New size should NOT be a negative number") + memset(_storage.memory.advanced(by: writerIndex), 0, _writerSize &- size) + _writerSize = size + } + + /// Clears the current size of the buffer + @inline(__always) + mutating public func clearSize() { + _writerSize = 0 + } + + /// Clears the current instance of the buffer, replacing it with new memory + @inline(__always) + mutating public func clear() { + _writerSize = 0 + alignment = 1 + _storage.initialize(for: _storage.capacity) + } + + /// Reads an object from the buffer + /// - Parameters: + /// - def: Type of the object + /// - position: the index of the object in the buffer + @inline(__always) + public func read(def: T.Type, position: Int) -> T { + if allowReadingUnalignedBuffers { + return _storage.memory.advanced(by: position).loadUnaligned(as: T.self) + } + return _storage.memory.advanced(by: position).load(as: T.self) + } + + /// Reads a slice from the memory assuming a type of T + /// - Parameters: + /// - index: index of the object to be read from the buffer + /// - count: count of bytes in memory + @inline(__always) + public func readSlice( + index: Int, + count: Int) -> [T] + { + assert( + index + count <= _storage.capacity, + "Reading out of bounds is illegal") + let start = _storage.memory.advanced(by: index) + .assumingMemoryBound(to: T.self) + let array = UnsafeBufferPointer(start: start, count: count) + return Array(array) + } + + #if !os(WASI) + /// Reads a string from the buffer and encodes it to a swift string + /// - Parameters: + /// - index: index of the string in the buffer + /// - count: length of the string + /// - type: Encoding of the string + @inline(__always) + public func readString( + at index: Int, + count: Int, + type: String.Encoding = .utf8) -> String? + { + assert( + index + count <= _storage.capacity, + "Reading out of bounds is illegal") + let start = _storage.memory.advanced(by: index) + .assumingMemoryBound(to: UInt8.self) + let bufprt = UnsafeBufferPointer(start: start, count: count) + return String(bytes: Array(bufprt), encoding: type) + } + #else + /// Reads a string from the buffer and encodes it to a swift string + /// - Parameters: + /// - index: index of the string in the buffer + /// - count: length of the string + @inline(__always) + public func readString( + at index: Int, + count: Int) -> String? + { + assert( + index + count <= _storage.capacity, + "Reading out of bounds is illegal") + let start = _storage.memory.advanced(by: index) + .assumingMemoryBound(to: UInt8.self) + let bufprt = UnsafeBufferPointer(start: start, count: count) + return String(cString: bufprt.baseAddress!) + } + #endif + + /// Creates a new Flatbuffer object that's duplicated from the current one + /// - Parameter removeBytes: the amount of bytes to remove from the current Size + @inline(__always) + public func duplicate(removing removeBytes: Int = 0) -> ByteBuffer { + assert(removeBytes > 0, "Can NOT remove negative bytes") + assert( + removeBytes < _storage.capacity, + "Can NOT remove more bytes than the ones allocated") + return ByteBuffer( + memory: _storage.memory, + count: _storage.capacity, + removing: _writerSize &- removeBytes) + } + + /// Returns the written bytes into the ``ByteBuffer`` + public var underlyingBytes: [UInt8] { + let cp = capacity &- writerIndex + let start = memory.advanced(by: writerIndex) + .bindMemory(to: UInt8.self, capacity: cp) + + let ptr = UnsafeBufferPointer(start: start, count: cp) + return Array(ptr) + } + + /// SkipPrefix Skips the first 4 bytes in case one of the following + /// functions are called `getPrefixedSizeCheckedRoot` & `getPrefixedSizeRoot` + /// which allows us to skip the first 4 bytes instead of recreating the buffer + @discardableResult + @usableFromInline + @inline(__always) + mutating func skipPrefix() -> Int32 { + _writerSize = _writerSize &- MemoryLayout.size + return read(def: Int32.self, position: 0) + } + +} + +extension ByteBuffer: CustomDebugStringConvertible { + + public var debugDescription: String { + """ + buffer located at: \(_storage.memory), with capacity of \(_storage.capacity) + { writerSize: \(_writerSize), readerSize: \(reader), writerIndex: \( + writerIndex) } + """ + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Constants.swift b/submodules/TelegramCore/FlatBuffers/Sources/Constants.swift new file mode 100644 index 0000000000..35912a50ca --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Constants.swift @@ -0,0 +1,114 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// A boolean to see if the system is littleEndian +let isLitteEndian: Bool = { + let number: UInt32 = 0x12345678 + return number == number.littleEndian +}() +/// Constant for the file id length +let FileIdLength = 4 +/// Type aliases +public typealias Byte = UInt8 +public typealias UOffset = UInt32 +public typealias SOffset = Int32 +public typealias VOffset = UInt16 +/// Maximum size for a buffer +public let FlatBufferMaxSize = UInt32 + .max << ((MemoryLayout.size * 8 - 1) - 1) + +/// Protocol that All Scalars should conform to +/// +/// Scalar is used to conform all the numbers that can be represented in a FlatBuffer. It's used to write/read from the buffer. +public protocol Scalar: Equatable { + associatedtype NumericValue + var convertedEndian: NumericValue { get } +} + +extension Scalar where Self: Verifiable {} + +extension Scalar where Self: FixedWidthInteger { + /// Converts the value from BigEndian to LittleEndian + /// + /// Converts values to little endian on machines that work with BigEndian, however this is NOT TESTED yet. + public var convertedEndian: NumericValue { + self as! Self.NumericValue + } +} + +extension Double: Scalar, Verifiable { + public typealias NumericValue = UInt64 + + public var convertedEndian: UInt64 { + bitPattern.littleEndian + } +} + +extension Float32: Scalar, Verifiable { + public typealias NumericValue = UInt32 + + public var convertedEndian: UInt32 { + bitPattern.littleEndian + } +} + +extension Bool: Scalar, Verifiable { + public var convertedEndian: UInt8 { + self == true ? 1 : 0 + } + + public typealias NumericValue = UInt8 +} + +extension Int: Scalar, Verifiable { + public typealias NumericValue = Int +} + +extension Int8: Scalar, Verifiable { + public typealias NumericValue = Int8 +} + +extension Int16: Scalar, Verifiable { + public typealias NumericValue = Int16 +} + +extension Int32: Scalar, Verifiable { + public typealias NumericValue = Int32 +} + +extension Int64: Scalar, Verifiable { + public typealias NumericValue = Int64 +} + +extension UInt8: Scalar, Verifiable { + public typealias NumericValue = UInt8 +} + +extension UInt16: Scalar, Verifiable { + public typealias NumericValue = UInt16 +} + +extension UInt32: Scalar, Verifiable { + public typealias NumericValue = UInt32 +} + +extension UInt64: Scalar, Verifiable { + public typealias NumericValue = UInt64 +} + +public func FlatBuffersVersion_24_12_23() {} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Enum.swift b/submodules/TelegramCore/FlatBuffers/Sources/Enum.swift new file mode 100644 index 0000000000..29b382247a --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Enum.swift @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Enum is a protocol that all flatbuffers enums should conform to +/// Since it allows us to get the actual `ByteSize` and `Value` from +/// a swift enum. +public protocol Enum { + /// associatedtype that the type of the enum should conform to + associatedtype T: Scalar & Verifiable + /// Size of the current associatedtype in the enum + static var byteSize: Int { get } + /// The current value the enum hosts + var value: T { get } +} + +extension Enum where Self: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + try verifier.inBuffer(position: position, of: type.self) + } + +} + +/// UnionEnum is a Protocol that allows us to create Union type of enums +/// and their value initializers. Since an `init` was required by +/// the verifier +public protocol UnionEnum: Enum { + init?(value: T) throws +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferBuilder.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferBuilder.swift new file mode 100644 index 0000000000..26ae634915 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferBuilder.swift @@ -0,0 +1,925 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// ``FlatBufferBuilder`` builds a `FlatBuffer` through manipulating its internal state. +/// +/// This is done by creating a ``ByteBuffer`` that hosts the incoming data and +/// has a hardcoded growth limit of `2GiB` which is set by the Flatbuffers standards. +/// +/// ```swift +/// var builder = FlatBufferBuilder() +/// ``` +/// The builder should be always created as a variable, since it would be passed into the writers +/// +@frozen +public struct FlatBufferBuilder { + + /// Storage for the Vtables used in the buffer are stored in here, so they would be written later in EndTable + @usableFromInline internal var _vtableStorage = VTableStorage() + /// Flatbuffer data will be written into + @usableFromInline internal var _bb: ByteBuffer + + /// Reference Vtables that were already written to the buffer + private var _vtables: [UOffset] = [] + /// A check if the buffer is being written into by a different table + private var isNested = false + /// Dictonary that stores a map of all the strings that were written to the buffer + private var stringOffsetMap: [String: Offset] = [:] + /// A check to see if finish(::) was ever called to retreive data object + private var finished = false + /// A check to see if the buffer should serialize Default values + private var serializeDefaults: Bool + + /// Current alignment for the buffer + var _minAlignment: Int = 0 { + didSet { + _bb.alignment = _minAlignment + } + } + + /// Gives a read access to the buffer's size + public var size: UOffset { _bb.size } + + #if !os(WASI) + /// Data representation of the buffer + /// + /// Should only be used after ``finish(offset:addPrefix:)`` is called + public var data: Data { + assert(finished, "Data shouldn't be called before finish()") + return Data( + bytes: _bb.memory.advanced(by: _bb.writerIndex), + count: _bb.capacity &- _bb.writerIndex) + } + #endif + + /// Returns the underlying bytes in the ``ByteBuffer`` + /// + /// Note: This should be used with caution. + public var fullSizedByteArray: [UInt8] { + let ptr = UnsafeBufferPointer( + start: _bb.memory.assumingMemoryBound(to: UInt8.self), + count: _bb.capacity) + return Array(ptr) + } + + /// Returns the written bytes into the ``ByteBuffer`` + /// + /// Should only be used after ``finish(offset:addPrefix:)`` is called + public var sizedByteArray: [UInt8] { + assert(finished, "Data shouldn't be called before finish()") + return _bb.underlyingBytes + } + + /// Returns the original ``ByteBuffer`` + /// + /// Returns the current buffer that was just created + /// with the offsets, and data written to it. + public var buffer: ByteBuffer { _bb } + + /// Returns a newly created sized ``ByteBuffer`` + /// + /// returns a new buffer that is sized to the data written + /// to the main buffer + public var sizedBuffer: ByteBuffer { + assert(finished, "Data shouldn't be called before finish()") + return ByteBuffer( + memory: _bb.memory.advanced(by: _bb.reader), + count: Int(_bb.size)) + } + + // MARK: - Init + + /// Initialize the buffer with a size + /// - Parameters: + /// - initialSize: Initial size for the buffer + /// - force: Allows default to be serialized into the buffer + /// + /// This initializes a new builder with an initialSize that would initialize + /// a new ``ByteBuffer``. ``FlatBufferBuilder`` by default doesnt serialize defaults + /// however the builder can be force by passing true for `serializeDefaults` + public init( + initialSize: Int32 = 1024, + serializeDefaults force: Bool = false) + { + assert(initialSize > 0, "Size should be greater than zero!") + guard isLitteEndian else { + fatalError( + "Reading/Writing a buffer in big endian machine is not supported on swift") + } + serializeDefaults = force + _bb = ByteBuffer(initialSize: Int(initialSize)) + } + + /// Clears the builder and the buffer from the written data. + mutating public func clear() { + _minAlignment = 0 + isNested = false + stringOffsetMap.removeAll(keepingCapacity: true) + _vtables.removeAll(keepingCapacity: true) + _vtableStorage.clear() + _bb.clear() + } + + // MARK: - Create Tables + + /// Checks if the required fields were serialized into the buffer + /// - Parameters: + /// - table: offset for the table + /// - fields: Array of all the important fields to be serialized + /// + /// *NOTE: Never call this function, this is only supposed to be called + /// by the generated code* + @inline(__always) + mutating public func require(table: Offset, fields: [Int32]) { + for index in stride(from: 0, to: fields.count, by: 1) { + let start = _bb.capacity &- Int(table.o) + let startTable = start &- Int(_bb.read(def: Int32.self, position: start)) + let isOkay = _bb.read( + def: VOffset.self, + position: startTable &+ Int(fields[index])) != 0 + assert(isOkay, "Flatbuffers requires the following field") + } + } + + /// Finished the buffer by adding the file id and then calling finish + /// - Parameters: + /// - offset: Offset of the table + /// - fileId: Takes the fileId + /// - prefix: if false it wont add the size of the buffer + /// + /// ``finish(offset:fileId:addPrefix:)`` should be called at the end of creating + /// a table + /// ```swift + /// var root = SomeObject + /// .createObject(&builder, + /// name: nameOffset) + /// builder.finish( + /// offset: root, + /// fileId: "ax1a", + /// addPrefix: true) + /// ``` + /// File id would append a file id name at the end of the written bytes before, + /// finishing the buffer. + /// + /// Whereas, if `addPrefix` is true, the written bytes would + /// include the size of the current buffer. + mutating public func finish( + offset: Offset, + fileId: String, + addPrefix prefix: Bool = false) + { + let size = MemoryLayout.size + preAlign( + len: size &+ (prefix ? size : 0) &+ FileIdLength, + alignment: _minAlignment) + assert(fileId.count == FileIdLength, "Flatbuffers requires file id to be 4") + _bb.push(string: fileId, len: 4) + finish(offset: offset, addPrefix: prefix) + } + + /// Finished the buffer by adding the file id, offset, and prefix to it. + /// - Parameters: + /// - offset: Offset of the table + /// - prefix: if false it wont add the size of the buffer + /// + /// ``finish(offset:addPrefix:)`` should be called at the end of creating + /// a table + /// ```swift + /// var root = SomeObject + /// .createObject(&builder, + /// name: nameOffset) + /// builder.finish( + /// offset: root, + /// addPrefix: true) + /// ``` + /// If `addPrefix` is true, the written bytes would + /// include the size of the current buffer. + mutating public func finish( + offset: Offset, + addPrefix prefix: Bool = false) + { + notNested() + let size = MemoryLayout.size + preAlign(len: size &+ (prefix ? size : 0), alignment: _minAlignment) + push(element: refer(to: offset.o)) + if prefix { push(element: _bb.size) } + _vtableStorage.clear() + finished = true + } + + /// ``startTable(with:)`` will let the builder know, that a new object is being serialized. + /// + /// The function will fatalerror if called while there is another object being serialized. + /// ```swift + /// let start = Monster + /// .startMonster(&fbb) + /// ``` + /// - Parameter numOfFields: Number of elements to be written to the buffer + /// - Returns: Offset of the newly started table + @inline(__always) + mutating public func startTable(with numOfFields: Int) -> UOffset { + notNested() + isNested = true + _vtableStorage.start(count: numOfFields) + return _bb.size + } + + /// ``endTable(at:)`` will let the ``FlatBufferBuilder`` know that the + /// object that's written to it is completed + /// + /// This would be called after all the elements are serialized, + /// it will add the current vtable into the ``ByteBuffer``. + /// The functions will `fatalError` in case the object is called + /// without ``startTable(with:)``, or the object has exceeded the limit of 2GB. + /// + /// - Parameter startOffset:Start point of the object written + /// - returns: The root of the table + mutating public func endTable(at startOffset: UOffset) -> UOffset { + assert(isNested, "Calling endtable without calling starttable") + let sizeofVoffset = MemoryLayout.size + let vTableOffset = push(element: SOffset(0)) + + let tableObjectSize = vTableOffset &- startOffset + assert(tableObjectSize < 0x10000, "Buffer can't grow beyond 2 Gigabytes") + let _max = Int(_vtableStorage.maxOffset) &+ sizeofVoffset + + _bb.fill(padding: _max) + _bb.write( + value: VOffset(tableObjectSize), + index: _bb.writerIndex &+ sizeofVoffset, + direct: true) + _bb.write(value: VOffset(_max), index: _bb.writerIndex, direct: true) + + var itr = 0 + while itr < _vtableStorage.writtenIndex { + let loaded = _vtableStorage.load(at: itr) + itr = itr &+ _vtableStorage.size + guard loaded.offset != 0 else { continue } + let _index = (_bb.writerIndex &+ Int(loaded.position)) + _bb.write( + value: VOffset(vTableOffset &- loaded.offset), + index: _index, + direct: true) + } + + _vtableStorage.clear() + let vt_use = _bb.size + + var isAlreadyAdded: Int? + + let vt2 = _bb.memory.advanced(by: _bb.writerIndex) + let len2 = vt2.load(fromByteOffset: 0, as: Int16.self) + + for index in stride(from: 0, to: _vtables.count, by: 1) { + let position = _bb.capacity &- Int(_vtables[index]) + let vt1 = _bb.memory.advanced(by: position) + let len1 = _bb.read(def: Int16.self, position: position) + if len2 != len1 || 0 != memcmp(vt1, vt2, Int(len2)) { continue } + + isAlreadyAdded = Int(_vtables[index]) + break + } + + if let offset = isAlreadyAdded { + let vTableOff = Int(vTableOffset) + let space = _bb.capacity &- vTableOff + _bb.write(value: Int32(offset &- vTableOff), index: space, direct: true) + _bb.pop(_bb.capacity &- space) + } else { + _bb.write(value: Int32(vt_use &- vTableOffset), index: Int(vTableOffset)) + _vtables.append(_bb.size) + } + isNested = false + return vTableOffset + } + + // MARK: - Builds Buffer + + /// Asserts to see if the object is not nested + @inline(__always) + @usableFromInline + mutating internal func notNested() { + assert(!isNested, "Object serialization must not be nested") + } + + /// Changes the minimuim alignment of the buffer + /// - Parameter size: size of the current alignment + @inline(__always) + @usableFromInline + mutating internal func minAlignment(size: Int) { + if size > _minAlignment { + _minAlignment = size + } + } + + /// Gets the padding for the current element + /// - Parameters: + /// - bufSize: Current size of the buffer + the offset of the object to be written + /// - elementSize: Element size + @inline(__always) + @usableFromInline + mutating internal func padding( + bufSize: UInt32, + elementSize: UInt32) -> UInt32 + { + ((~bufSize) &+ 1) & (elementSize - 1) + } + + /// Prealigns the buffer before writting a new object into the buffer + /// - Parameters: + /// - len:Length of the object + /// - alignment: Alignment type + @inline(__always) + @usableFromInline + mutating internal func preAlign(len: Int, alignment: Int) { + minAlignment(size: alignment) + _bb.fill(padding: Int(padding( + bufSize: _bb.size &+ UOffset(len), + elementSize: UOffset(alignment)))) + } + + /// Prealigns the buffer before writting a new object into the buffer + /// - Parameters: + /// - len: Length of the object + /// - type: Type of the object to be written + @inline(__always) + @usableFromInline + mutating internal func preAlign(len: Int, type: T.Type) { + preAlign(len: len, alignment: MemoryLayout.size) + } + + /// Refers to an object that's written in the buffer + /// - Parameter off: the objects index value + @inline(__always) + @usableFromInline + mutating internal func refer(to off: UOffset) -> UOffset { + let size = MemoryLayout.size + preAlign(len: size, alignment: size) + return _bb.size &- off &+ UInt32(size) + } + + /// Tracks the elements written into the buffer + /// - Parameters: + /// - offset: The offset of the element witten + /// - position: The position of the element + @inline(__always) + @usableFromInline + mutating internal func track(offset: UOffset, at position: VOffset) { + _vtableStorage.add(loc: (offset: offset, position: position)) + } + + // MARK: - Inserting Vectors + + /// ``startVector(_:elementSize:)`` creates a new vector within buffer + /// + /// The function checks if there is a current object being written, if + /// the check passes it creates a buffer alignment of `length * elementSize` + /// ```swift + /// builder.startVector( + /// int32Values.count, elementSize: 4) + /// ``` + /// + /// - Parameters: + /// - len: Length of vector to be created + /// - elementSize: Size of object type to be written + @inline(__always) + mutating public func startVector(_ len: Int, elementSize: Int) { + notNested() + isNested = true + preAlign(len: len &* elementSize, type: UOffset.self) + preAlign(len: len &* elementSize, alignment: elementSize) + } + + /// ``endVector(len:)`` ends the currently created vector + /// + /// Calling ``endVector(len:)`` requires the length, of the current + /// vector. The length would be pushed to indicate the count of numbers + /// within the vector. If ``endVector(len:)`` is called without + /// ``startVector(_:elementSize:)`` it asserts. + /// + /// ```swift + /// let vectorOffset = builder. + /// endVector(len: int32Values.count) + /// ``` + /// + /// - Parameter len: Length of the buffer + /// - Returns: Returns the current ``Offset`` in the ``ByteBuffer`` + @inline(__always) + mutating public func endVector(len: Int) -> Offset { + assert(isNested, "Calling endVector without calling startVector") + isNested = false + return Offset(offset: push(element: Int32(len))) + } + + /// Creates a vector of type ``Scalar`` into the ``ByteBuffer`` + /// + /// ``createVector(_:)-4swl0`` writes a vector of type Scalars into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([1, 2, 3, 4]) + /// ``` + /// + /// The underlying implementation simply calls ``createVector(_:size:)-4lhrv`` + /// + /// - Parameter elements: elements to be written into the buffer + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(_ elements: [T]) -> Offset { + createVector(elements, size: elements.count) + } + + /// Creates a vector of type Scalar in the buffer + /// + /// ``createVector(_:)-4swl0`` writes a vector of type Scalars into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([1, 2, 3, 4], size: 4) + /// ``` + /// + /// - Parameter elements: Elements to be written into the buffer + /// - Parameter size: Count of elements + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector( + _ elements: [T], + size: Int) -> Offset + { + let size = size + startVector(size, elementSize: MemoryLayout.size) + _bb.push(elements: elements) + return endVector(len: size) + } + + #if swift(>=5.0) && !os(WASI) + @inline(__always) + /// Creates a vector of bytes in the buffer. + /// + /// Allows creating a vector from `Data` without copying to a `[UInt8]` + /// + /// - Parameter bytes: bytes to be written into the buffer + /// - Returns: ``Offset`` of the vector + mutating public func createVector(bytes: ContiguousBytes) -> Offset { + let size = bytes.withUnsafeBytes { ptr in ptr.count } + startVector(size, elementSize: MemoryLayout.size) + _bb.push(bytes: bytes) + return endVector(len: size) + } + #endif + + /// Creates a vector of type ``Enum`` into the ``ByteBuffer`` + /// + /// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([.swift, .cpp]) + /// ``` + /// + /// The underlying implementation simply calls ``createVector(_:size:)-7cx6z`` + /// + /// - Parameter elements: elements to be written into the buffer + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(_ elements: [T]) -> Offset { + createVector(elements, size: elements.count) + } + + /// Creates a vector of type ``Enum`` into the ``ByteBuffer`` + /// + /// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([.swift, .cpp]) + /// ``` + /// + /// - Parameter elements: Elements to be written into the buffer + /// - Parameter size: Count of elements + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector( + _ elements: [T], + size: Int) -> Offset + { + let size = size + startVector(size, elementSize: T.byteSize) + for index in stride(from: elements.count, to: 0, by: -1) { + _bb.push(value: elements[index &- 1].value, len: T.byteSize) + } + return endVector(len: size) + } + + /// Creates a vector of already written offsets + /// + /// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)``. + /// + /// The underlying implementation simply calls ``createVector(ofOffsets:len:)`` + /// + /// ```swift + /// let namesOffsets = builder. + /// createVector(ofOffsets: [name1, name2]) + /// ``` + /// - Parameter offsets: Array of offsets of type ``Offset`` + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(ofOffsets offsets: [Offset]) -> Offset { + createVector(ofOffsets: offsets, len: offsets.count) + } + + /// Creates a vector of already written offsets + /// + /// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// + /// ```swift + /// let namesOffsets = builder. + /// createVector(ofOffsets: [name1, name2]) + /// ``` + /// + /// - Parameter offsets: Array of offsets of type ``Offset`` + /// - Parameter size: Count of elements + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector( + ofOffsets offsets: [Offset], + len: Int) -> Offset + { + startVector(len, elementSize: MemoryLayout.size) + for index in stride(from: offsets.count, to: 0, by: -1) { + push(element: offsets[index &- 1]) + } + return endVector(len: len) + } + + /// Creates a vector of strings + /// + /// ``createVector(ofStrings:)`` creates a vector of `String` into + /// ``ByteBuffer``. This is a convenient method instead of manually + /// creating the string offsets, you simply pass it to this function + /// and it would write the strings into the ``ByteBuffer``. + /// After that it calls ``createVector(ofOffsets:)`` + /// + /// ```swift + /// let namesOffsets = builder. + /// createVector(ofStrings: ["Name", "surname"]) + /// ``` + /// + /// - Parameter str: Array of string + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(ofStrings str: [String]) -> Offset { + var offsets: [Offset] = [] + for index in stride(from: 0, to: str.count, by: 1) { + offsets.append(create(string: str[index])) + } + return createVector(ofOffsets: offsets) + } + + /// Creates a vector of type ``NativeStruct``. + /// + /// Any swift struct in the generated code, should confirm to + /// ``NativeStruct``. Since the generated swift structs are padded + /// to the `FlatBuffers` standards. + /// + /// ```swift + /// let offsets = builder. + /// createVector(ofStructs: [NativeStr(num: 1), NativeStr(num: 2)]) + /// ``` + /// + /// - Parameter structs: A vector of ``NativeStruct`` + /// - Returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(ofStructs structs: [T]) + -> Offset + { + startVector( + structs.count * MemoryLayout.size, + elementSize: MemoryLayout.alignment) + _bb.push(elements: structs) + return endVector(len: structs.count) + } + + // MARK: - Inserting Structs + + /// Writes a ``NativeStruct`` into the ``ByteBuffer`` + /// + /// Adds a native struct that's build and padded according + /// to `FlatBuffers` standards. with a predefined position. + /// + /// ```swift + /// let offset = builder.create( + /// struct: NativeStr(num: 1), + /// position: 10) + /// ``` + /// + /// - Parameters: + /// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer`` + /// - position: The predefined position of the object + /// - Returns: ``Offset`` of written struct + @inline(__always) + @discardableResult + mutating public func create( + struct s: T, position: VOffset) -> Offset + { + let offset = create(struct: s) + _vtableStorage.add( + loc: (offset: _bb.size, position: VOffset(position))) + return offset + } + + /// Writes a ``NativeStruct`` into the ``ByteBuffer`` + /// + /// Adds a native struct that's build and padded according + /// to `FlatBuffers` standards, directly into the buffer without + /// a predefined position. + /// + /// ```swift + /// let offset = builder.create( + /// struct: NativeStr(num: 1)) + /// ``` + /// + /// - Parameters: + /// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer`` + /// - Returns: ``Offset`` of written struct + @inline(__always) + @discardableResult + mutating public func create( + struct s: T) -> Offset + { + let size = MemoryLayout.size + preAlign(len: size, alignment: MemoryLayout.alignment) + _bb.push(struct: s, size: size) + return Offset(offset: _bb.size) + } + + // MARK: - Inserting Strings + + /// Insets a string into the buffer of type `UTF8` + /// + /// Adds a swift string into ``ByteBuffer`` by encoding it + /// using `UTF8` + /// + /// ```swift + /// let nameOffset = builder + /// .create(string: "welcome") + /// ``` + /// + /// - Parameter str: String to be serialized + /// - returns: ``Offset`` of inserted string + @inline(__always) + mutating public func create(string str: String?) -> Offset { + guard let str = str else { return Offset() } + let len = str.utf8.count + notNested() + preAlign(len: len &+ 1, type: UOffset.self) + _bb.fill(padding: 1) + _bb.push(string: str, len: len) + push(element: UOffset(len)) + return Offset(offset: _bb.size) + } + + /// Insets a shared string into the buffer of type `UTF8` + /// + /// Adds a swift string into ``ByteBuffer`` by encoding it + /// using `UTF8`. The function will check if the string, + /// is already written to the ``ByteBuffer`` + /// + /// ```swift + /// let nameOffset = builder + /// .createShared(string: "welcome") + /// + /// + /// let secondOffset = builder + /// .createShared(string: "welcome") + /// + /// assert(nameOffset.o == secondOffset.o) + /// ``` + /// + /// - Parameter str: String to be serialized + /// - returns: ``Offset`` of inserted string + @inline(__always) + mutating public func createShared(string str: String?) -> Offset { + guard let str = str else { return Offset() } + if let offset = stringOffsetMap[str] { + return offset + } + let offset = create(string: str) + stringOffsetMap[str] = offset + return offset + } + + // MARK: - Inseting offsets + + /// Writes the ``Offset`` of an already written table + /// + /// Writes the ``Offset`` of a table if not empty into the + /// ``ByteBuffer`` + /// + /// - Parameters: + /// - offset: ``Offset`` of another object to be written + /// - position: The predefined position of the object + @inline(__always) + mutating public func add(offset: Offset, at position: VOffset) { + if offset.isEmpty { return } + add(element: refer(to: offset.o), def: 0, at: position) + } + + /// Pushes a value of type ``Offset`` into the ``ByteBuffer`` + /// - Parameter o: ``Offset`` + /// - returns: Current position of the ``Offset`` + @inline(__always) + @discardableResult + mutating public func push(element o: Offset) -> UOffset { + push(element: refer(to: o.o)) + } + + // MARK: - Inserting Scalars to Buffer + + /// Writes a ``Scalar`` value into ``ByteBuffer`` + /// + /// ``add(element:def:at:)`` takes in a default value, and current value + /// and the position within the `VTable`. The default value would not + /// be serialized if the value is the same as the current value or + /// `serializeDefaults` is equal to false. + /// + /// If serializing defaults is important ``init(initialSize:serializeDefaults:)``, + /// passing true for `serializeDefaults` would do the job. + /// + /// ```swift + /// // Adds 10 to the buffer + /// builder.add(element: Int(10), def: 1, position 12) + /// ``` + /// + /// *NOTE: Never call this manually* + /// + /// - Parameters: + /// - element: Element to insert + /// - def: Default value for that element + /// - position: The predefined position of the element + @inline(__always) + mutating public func add( + element: T, + def: T, + at position: VOffset) + { + if element == def && !serializeDefaults { return } + track(offset: push(element: element), at: position) + } + + /// Writes a optional ``Scalar`` value into ``ByteBuffer`` + /// + /// Takes an optional value to be written into the ``ByteBuffer`` + /// + /// *NOTE: Never call this manually* + /// + /// - Parameters: + /// - element: Optional element of type scalar + /// - position: The predefined position of the element + @inline(__always) + mutating public func add(element: T?, at position: VOffset) { + guard let element = element else { return } + track(offset: push(element: element), at: position) + } + + /// Pushes a values of type ``Scalar`` into the ``ByteBuffer`` + /// + /// *NOTE: Never call this manually* + /// + /// - Parameter element: Element to insert + /// - returns: position of the Element + @inline(__always) + @discardableResult + mutating public func push(element: T) -> UOffset { + let size = MemoryLayout.size + preAlign( + len: size, + alignment: size) + _bb.push(value: element, len: size) + return _bb.size + } + +} + +extension FlatBufferBuilder: CustomDebugStringConvertible { + + public var debugDescription: String { + """ + buffer debug: + \(_bb) + builder debug: + { finished: \(finished), serializeDefaults: \( + serializeDefaults), isNested: \(isNested) } + """ + } + + typealias FieldLoc = (offset: UOffset, position: VOffset) + + /// VTableStorage is a class to contain the VTable buffer that would be serialized into buffer + @usableFromInline + internal class VTableStorage { + /// Memory check since deallocating each time we want to clear would be expensive + /// and memory leaks would happen if we dont deallocate the first allocated memory. + /// memory is promised to be available before adding `FieldLoc` + private var memoryInUse = false + /// Size of FieldLoc in memory + let size = MemoryLayout.stride + /// Memeory buffer + var memory: UnsafeMutableRawBufferPointer! + /// Capacity of the current buffer + var capacity: Int = 0 + /// Maximuim offset written to the class + var maxOffset: VOffset = 0 + /// number of fields written into the buffer + var numOfFields: Int = 0 + /// Last written Index + var writtenIndex: Int = 0 + + /// Creates the memory to store the buffer in + @usableFromInline + @inline(__always) + init() { + memory = UnsafeMutableRawBufferPointer.allocate( + byteCount: 0, + alignment: 0) + } + + @inline(__always) + deinit { + memory.deallocate() + } + + /// Builds a buffer with byte count of fieldloc.size * count of field numbers + /// - Parameter count: number of fields to be written + @inline(__always) + func start(count: Int) { + assert(count >= 0, "number of fields should NOT be negative") + let capacity = count &* size + ensure(space: capacity) + } + + /// Adds a FieldLoc into the buffer, which would track how many have been written, + /// and max offset + /// - Parameter loc: Location of encoded element + @inline(__always) + func add(loc: FieldLoc) { + memory.baseAddress?.advanced(by: writtenIndex).storeBytes( + of: loc, + as: FieldLoc.self) + writtenIndex = writtenIndex &+ size + numOfFields = numOfFields &+ 1 + maxOffset = max(loc.position, maxOffset) + } + + /// Clears the data stored related to the encoded buffer + @inline(__always) + func clear() { + maxOffset = 0 + numOfFields = 0 + writtenIndex = 0 + } + + /// Ensure that the buffer has enough space instead of recreating the buffer each time. + /// - Parameter space: space required for the new vtable + @inline(__always) + func ensure(space: Int) { + guard space &+ writtenIndex > capacity else { return } + memory.deallocate() + memory = UnsafeMutableRawBufferPointer.allocate( + byteCount: space, + alignment: size) + capacity = space + } + + /// Loads an object of type `FieldLoc` from buffer memory + /// - Parameter index: index of element + /// - Returns: a FieldLoc at index + @inline(__always) + func load(at index: Int) -> FieldLoc { + memory.load(fromByteOffset: index, as: FieldLoc.self) + } + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferObject.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferObject.swift new file mode 100644 index 0000000000..e836e6120c --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferObject.swift @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// NativeStruct is a protocol that indicates if the struct is a native `swift` struct +/// since now we will be serializing native structs into the buffer. +public protocol NativeStruct {} + +/// FlatbuffersInitializable is a protocol that allows any object to be +/// Initialized from a ByteBuffer +public protocol FlatbuffersInitializable { + /// Any flatbuffers object that confirms to this protocol is going to be + /// initializable through this initializer + init(_ bb: ByteBuffer, o: Int32) +} + +/// FlatbufferObject structures all the Flatbuffers objects +public protocol FlatBufferObject: FlatbuffersInitializable { + var __buffer: ByteBuffer! { get } +} + +/// ``ObjectAPIPacker`` is a protocol that allows object to pack and unpack from a +/// ``NativeObject`` to a flatbuffers Object and vice versa. +public protocol ObjectAPIPacker { + /// associatedtype to the object that should be unpacked. + associatedtype T + + /// ``pack(_:obj:)-3ptws`` tries to pacs the variables of a native Object into the `ByteBuffer` by using + /// a FlatBufferBuilder + /// - Parameters: + /// - builder: FlatBufferBuilder that will host incoming data + /// - obj: Object of associatedtype to the current implementer + /// + /// ``pack(_:obj:)-3ptws`` can be called by passing through an already initialized ``FlatBufferBuilder`` + /// or it can be called by using the public API that will create a new ``FlatBufferBuilder`` + static func pack(_ builder: inout FlatBufferBuilder, obj: inout T?) -> Offset + + /// ``pack(_:obj:)-20ipk`` packs the variables of a native Object into the `ByteBuffer` by using + /// the FlatBufferBuilder + /// - Parameters: + /// - builder: FlatBufferBuilder that will host incoming data + /// - obj: Object of associatedtype to the current implementer + /// + /// ``pack(_:obj:)-20ipk`` can be called by passing through an already initialized ``FlatBufferBuilder`` + /// or it can be called by using the public API that will create a new ``FlatBufferBuilder`` + static func pack(_ builder: inout FlatBufferBuilder, obj: inout T) -> Offset + + /// ``unpack()`` unpacks a ``FlatBuffers`` object into a Native swift object. + mutating func unpack() -> T +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatBuffersUtils.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatBuffersUtils.swift new file mode 100644 index 0000000000..18c130f5a2 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatBuffersUtils.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// FlatBuffersUtils hosts some utility functions that might be useful +public enum FlatBuffersUtils { + + /// Gets the size of the prefix + /// - Parameter bb: Flatbuffer object + public static func getSizePrefix(bb: ByteBuffer) -> Int32 { + bb.read(def: Int32.self, position: bb.reader) + } + + /// Removes the prefix by duplicating the Flatbuffer this call is expensive since its + /// creates a new buffer use `readPrefixedSizeCheckedRoot` instead + /// unless a completely new buffer is required + /// - Parameter bb: Flatbuffer object + /// + /// + public static func removeSizePrefix(bb: ByteBuffer) -> ByteBuffer { + bb.duplicate(removing: MemoryLayout.size) + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatbuffersErrors.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatbuffersErrors.swift new file mode 100644 index 0000000000..13207b53a9 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatbuffersErrors.swift @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Collection of thrown from the Flatbuffer verifier +public enum FlatbuffersErrors: Error, Equatable { + + /// Thrown when trying to verify a buffer that doesnt have the length of an ID + case bufferDoesntContainID + /// Thrown when verifying a file id that doesnt match buffer id + case bufferIdDidntMatchPassedId + /// Prefixed size doesnt match the current (readable) buffer size + case prefixedSizeNotEqualToBufferSize + /// Thrown when buffer is bigger than the allowed 2GiB + case exceedsMaxSizeAllowed + /// Thrown when there is an missaligned pointer at position + /// of type + case missAlignedPointer(position: Int, type: String) + /// Thrown when trying to read a value that goes out of the + /// current buffer bounds + case outOfBounds(position: UInt, end: Int) + /// Thrown when the signed offset is out of the bounds of the + /// current buffer + case signedOffsetOutOfBounds(offset: Int, position: Int) + /// Thrown when a required field doesnt exist within the buffer + case requiredFieldDoesntExist(position: VOffset, name: String) + /// Thrown when a string is missing its NULL Terminator `\0`, + /// this can be disabled in the `VerifierOptions` + case missingNullTerminator(position: Int, str: String?) + /// Thrown when the verifier has reached the maximum tables allowed, + /// this can be disabled in the `VerifierOptions` + case maximumTables + /// Thrown when the verifier has reached the maximum depth allowed, + /// this can be disabled in the `VerifierOptions` + case maximumDepth + /// Thrown when the verifier is presented with an unknown union case + case unknownUnionCase + /// thrown when a value for a union is not found within the buffer + case valueNotFound(key: Int?, keyName: String, field: Int?, fieldName: String) + /// thrown when the size of the keys vector doesnt match fields vector + case unionVectorSize( + keyVectorSize: Int, + fieldVectorSize: Int, + unionKeyName: String, + fieldName: String) + case apparentSizeTooLarge + +} + +#if !os(WASI) + +extension FlatbuffersErrors { + public static func == ( + lhs: FlatbuffersErrors, + rhs: FlatbuffersErrors) -> Bool + { + lhs.localizedDescription == rhs.localizedDescription + } +} + +#endif diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Int+extension.swift b/submodules/TelegramCore/FlatBuffers/Sources/Int+extension.swift new file mode 100644 index 0000000000..62b5cd5cd1 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Int+extension.swift @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +extension Int { + + /// Moves the current int into the nearest power of two + /// + /// This is used since the UnsafeMutableRawPointer will face issues when writing/reading + /// if the buffer alignment exceeds that actual size of the buffer + var convertToPowerofTwo: Int { + guard self > 0 else { return 1 } + var n = UOffset(self) + + #if arch(arm) || arch(i386) + let max = UInt32(Int.max) + #else + let max = UInt32.max + #endif + + n -= 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + if n != max { + n += 1 + } + + return Int(n) + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Message.swift b/submodules/TelegramCore/FlatBuffers/Sources/Message.swift new file mode 100644 index 0000000000..8ccfca4186 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Message.swift @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// FlatBufferGRPCMessage protocol that should allow us to invoke +/// initializers directly from the GRPC generated code +public protocol FlatBufferGRPCMessage { + + /// Raw pointer which would be pointing to the beginning of the readable bytes + var rawPointer: UnsafeMutableRawPointer { get } + + /// Size of readable bytes in the buffer + var size: Int { get } + + init(byteBuffer: ByteBuffer) +} + +/// Message is a wrapper around Buffers to to able to send Flatbuffers `Buffers` through the +/// GRPC library +public struct Message: FlatBufferGRPCMessage { + internal var buffer: ByteBuffer + + /// Returns the an object of type T that would be read from the buffer + public var object: T { + T.init( + buffer, + o: Int32(buffer.read(def: UOffset.self, position: buffer.reader)) + + Int32(buffer.reader)) + } + + public var rawPointer: UnsafeMutableRawPointer { + buffer.memory.advanced(by: buffer.reader) } + + public var size: Int { Int(buffer.size) } + + /// Initializes the message with the type Flatbuffer.Bytebuffer that is transmitted over + /// GRPC + /// - Parameter byteBuffer: Flatbuffer ByteBuffer object + public init(byteBuffer: ByteBuffer) { + buffer = byteBuffer + } + + /// Initializes the message by copying the buffer to the message to be sent. + /// from the builder + /// - Parameter builder: FlatbufferBuilder that has the bytes created in + /// - Note: Use `builder.finish(offset)` before passing the builder without prefixing anything to it + public init(builder: inout FlatBufferBuilder) { + buffer = builder.sizedBuffer + builder.clear() + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Mutable.swift b/submodules/TelegramCore/FlatBuffers/Sources/Mutable.swift new file mode 100644 index 0000000000..307e9a927c --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Mutable.swift @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Mutable is a protocol that allows us to mutate Scalar values within a ``ByteBuffer`` +public protocol Mutable { + /// makes Flatbuffer accessed within the Protocol + var bb: ByteBuffer { get } + /// makes position of the ``Table``/``Struct`` accessed within the Protocol + var position: Int32 { get } +} + +extension Mutable { + + /// Mutates the memory in the buffer, this is only called from the access function of ``Table`` and ``struct`` + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + func mutate(value: T, o: Int32) -> Bool { + guard o != 0 else { return false } + bb.write(value: value, index: Int(o), direct: true) + return true + } +} + +extension Mutable where Self == Table { + + /// Mutates a value by calling mutate with respect to the position in a ``Table`` + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func mutate(_ value: T, index: Int32) -> Bool { + guard index != 0 else { return false } + return mutate(value: value, o: index + position) + } + + /// Directly mutates the element by calling mutate + /// + /// Mutates the Element at index ignoring the current position by calling mutate + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func directMutate(_ value: T, index: Int32) -> Bool { + mutate(value: value, o: index) + } +} + +extension Mutable where Self == Struct { + + /// Mutates a value by calling mutate with respect to the position in the struct + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func mutate(_ value: T, index: Int32) -> Bool { + mutate(value: value, o: index + position) + } + + /// Directly mutates the element by calling mutate + /// + /// Mutates the Element at index ignoring the current position by calling mutate + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func directMutate(_ value: T, index: Int32) -> Bool { + mutate(value: value, o: index) + } +} + +extension Struct: Mutable {} +extension Table: Mutable {} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/NativeObject.swift b/submodules/TelegramCore/FlatBuffers/Sources/NativeObject.swift new file mode 100644 index 0000000000..2ed83970ff --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/NativeObject.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// NativeObject is a protocol that all of the `Object-API` generated code should be +/// conforming to since it allows developers the ease of use to pack and unpack their +/// Flatbuffers objects +public protocol NativeObject {} + +extension NativeObject { + + /// Serialize is a helper function that serializes the data from the Object API to a bytebuffer directly th + /// - Parameter type: Type of the Flatbuffer object + /// - Returns: returns the encoded sized ByteBuffer + public func serialize(type: T.Type) -> ByteBuffer + where T.T == Self + { + var builder = FlatBufferBuilder(initialSize: 1024) + return serialize(builder: &builder, type: type.self) + } + + /// Serialize is a helper function that serializes the data from the Object API to a bytebuffer directly. + /// + /// - Parameters: + /// - builder: A FlatBufferBuilder + /// - type: Type of the Flatbuffer object + /// - Returns: returns the encoded sized ByteBuffer + /// - Note: The `serialize(builder:type)` can be considered as a function that allows you to create smaller builder instead of the default `1024`. + /// It can be considered less expensive in terms of memory allocation + public func serialize( + builder: inout FlatBufferBuilder, + type: T.Type) -> ByteBuffer where T.T == Self + { + var s = self + let root = type.pack(&builder, obj: &s) + builder.finish(offset: root) + return builder.sizedBuffer + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Offset.swift b/submodules/TelegramCore/FlatBuffers/Sources/Offset.swift new file mode 100644 index 0000000000..95ef9df993 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Offset.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Offset object for all the Objects that are written into the buffer +public struct Offset { + /// Offset of the object in the buffer + public var o: UOffset + /// Returns false if the offset is equal to zero + public var isEmpty: Bool { o == 0 } + + public init(offset: UOffset) { o = offset } + public init() { o = 0 } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Root.swift b/submodules/TelegramCore/FlatBuffers/Sources/Root.swift new file mode 100644 index 0000000000..8e606e6ccf --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Root.swift @@ -0,0 +1,116 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Takes in a prefixed sized buffer, where the prefixed size would be skipped. +/// And would verify that the buffer passed is a valid `Flatbuffers` Object. +/// - Parameters: +/// - byteBuffer: Buffer that needs to be checked and read +/// - options: Verifier options +/// - Throws: FlatbuffersErrors +/// - Returns: Returns a valid, checked Flatbuffers object +/// +/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in +/// the ``ByteBuffer`` and verifies the buffer by calling ``getCheckedRoot(byteBuffer:options:)`` +public func getPrefixedSizeCheckedRoot( + byteBuffer: inout ByteBuffer, + fileId: String? = nil, + options: VerifierOptions = .init()) throws -> T +{ + byteBuffer.skipPrefix() + return try getCheckedRoot( + byteBuffer: &byteBuffer, + fileId: fileId, + options: options) +} + +/// Takes in a prefixed sized buffer, where we check if the sized buffer is equal to prefix size. +/// And would verify that the buffer passed is a valid `Flatbuffers` Object. +/// - Parameters: +/// - byteBuffer: Buffer that needs to be checked and read +/// - options: Verifier options +/// - Throws: FlatbuffersErrors +/// - Returns: Returns a valid, checked Flatbuffers object +/// +/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in +/// the ``ByteBuffer`` and verifies the buffer by calling ``getCheckedRoot(byteBuffer:options:)`` +public func getCheckedPrefixedSizeRoot( + byteBuffer: inout ByteBuffer, + fileId: String? = nil, + options: VerifierOptions = .init()) throws -> T +{ + let prefix = byteBuffer.skipPrefix() + if prefix != byteBuffer.size { + throw FlatbuffersErrors.prefixedSizeNotEqualToBufferSize + } + return try getCheckedRoot( + byteBuffer: &byteBuffer, + fileId: fileId, + options: options) +} + +/// Takes in a prefixed sized buffer, where the prefixed size would be skipped. +/// Returns a `NON-Checked` flatbuffers object +/// - Parameter byteBuffer: Buffer that contains data +/// - Returns: Returns a Flatbuffers object +/// +/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in +/// the ``ByteBuffer`` and then calls ``getRoot(byteBuffer:)`` +public func getPrefixedSizeRoot( + byteBuffer: inout ByteBuffer) + -> T +{ + byteBuffer.skipPrefix() + return getRoot(byteBuffer: &byteBuffer) + +} + +/// Verifies that the buffer passed is a valid `Flatbuffers` Object. +/// - Parameters: +/// - byteBuffer: Buffer that needs to be checked and read +/// - options: Verifier options +/// - Throws: FlatbuffersErrors +/// - Returns: Returns a valid, checked Flatbuffers object +/// +/// ``getCheckedRoot(byteBuffer:options:)`` Takes in a ``ByteBuffer`` and verifies +/// that by creating a ``Verifier`` and checkes if all the `Bytes` and correctly aligned +/// and within the ``ByteBuffer`` range. +public func getCheckedRoot( + byteBuffer: inout ByteBuffer, + fileId: String? = nil, + options: VerifierOptions = .init()) throws -> T +{ + var verifier = try Verifier(buffer: &byteBuffer, options: options) + if let fileId = fileId { + try verifier.verify(id: fileId) + } + try ForwardOffset.verify(&verifier, at: 0, of: T.self) + return T.init( + byteBuffer, + o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) + + Int32(byteBuffer.reader)) +} + +/// Returns a `NON-Checked` flatbuffers object +/// - Parameter byteBuffer: Buffer that contains data +/// - Returns: Returns a Flatbuffers object +public func getRoot(byteBuffer: inout ByteBuffer) -> T { + T.init( + byteBuffer, + o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) + + Int32(byteBuffer.reader)) +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/String+extension.swift b/submodules/TelegramCore/FlatBuffers/Sources/String+extension.swift new file mode 100644 index 0000000000..de4f5f91f0 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/String+extension.swift @@ -0,0 +1,109 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +extension String: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer`, `missingNullTerminator` and `outOfBounds` + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + + let range = try String.verifyRange(&verifier, at: position, of: UInt8.self) + /// Safe &+ since we already check for overflow in verify range + let stringLen = range.start &+ range.count + + if stringLen >= verifier.capacity { + throw FlatbuffersErrors.outOfBounds( + position: UInt(clamping: stringLen.magnitude), + end: verifier.capacity) + } + + let isNullTerminated = verifier._buffer.read( + def: UInt8.self, + position: stringLen) == 0 + + if !verifier._options._ignoreMissingNullTerminators && !isNullTerminated { + let str = verifier._buffer.readString(at: range.start, count: range.count) + throw FlatbuffersErrors.missingNullTerminator( + position: position, + str: str) + } + } +} + +extension String: FlatbuffersInitializable { + + /// Initailizes a string from a Flatbuffers ByteBuffer + /// - Parameters: + /// - bb: ByteBuffer containing the readable string + /// - o: Current position + public init(_ bb: ByteBuffer, o: Int32) { + let v = Int(o) + let count = bb.read(def: Int32.self, position: v) + self = bb.readString( + at: MemoryLayout.size + v, + count: Int(count)) ?? "" + } +} + +extension String: ObjectAPIPacker { + + public static func pack( + _ builder: inout FlatBufferBuilder, + obj: inout String?) -> Offset + { + guard var obj = obj else { return Offset() } + return pack(&builder, obj: &obj) + } + + public static func pack( + _ builder: inout FlatBufferBuilder, + obj: inout String) -> Offset + { + builder.create(string: obj) + } + + public mutating func unpack() -> String { + self + } + +} + +extension String: NativeObject { + + public func serialize(type: T.Type) -> ByteBuffer + where T.T == Self + { + fatalError("serialize should never be called from string directly") + } + + public func serialize( + builder: inout FlatBufferBuilder, + type: T.Type) -> ByteBuffer where T.T == Self + { + fatalError("serialize should never be called from string directly") + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Struct.swift b/submodules/TelegramCore/FlatBuffers/Sources/Struct.swift new file mode 100644 index 0000000000..bbce8f978c --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Struct.swift @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Struct is a representation of a mutable `Flatbuffers` struct +/// since native structs are value types and cant be mutated +@frozen +public struct Struct { + + /// Hosting Bytebuffer + public private(set) var bb: ByteBuffer + /// Current position of the struct + public private(set) var position: Int32 + + /// Initializer for a mutable flatbuffers struct + /// - Parameters: + /// - bb: Current hosting Bytebuffer + /// - position: Current position for the struct in the ByteBuffer + public init(bb: ByteBuffer, position: Int32 = 0) { + self.bb = bb + self.position = position + } + + /// Reads data from the buffer directly at offset O + /// - Parameters: + /// - type: Type of data to be read + /// - o: Current offset of the data + /// - Returns: Data of Type T that conforms to type Scalar + public func readBuffer(of type: T.Type, at o: Int32) -> T { + let r = bb.read(def: T.self, position: Int(o + position)) + return r + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Table.swift b/submodules/TelegramCore/FlatBuffers/Sources/Table.swift new file mode 100644 index 0000000000..02a2e6f2cd --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Table.swift @@ -0,0 +1,236 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// `Table` is a Flatbuffers object that can read, +/// mutate scalar fields within a valid flatbuffers buffer +@frozen +public struct Table { + + /// Hosting Bytebuffer + public private(set) var bb: ByteBuffer + /// Current position of the table within the buffer + public private(set) var position: Int32 + + /// Initializer for the table interface to allow generated code to read + /// data from memory + /// - Parameters: + /// - bb: ByteBuffer that stores data + /// - position: Current table position + /// - Note: This will `CRASH` if read on a big endian machine + public init(bb: ByteBuffer, position: Int32 = 0) { + guard isLitteEndian else { + fatalError( + "Reading/Writing a buffer in big endian machine is not supported on swift") + } + self.bb = bb + self.position = position + } + + /// Gets the offset of the current field within the buffer by reading + /// the vtable + /// - Parameter o: current offset + /// - Returns: offset of field within buffer + public func offset(_ o: Int32) -> Int32 { + let vtable = position - bb.read(def: Int32.self, position: Int(position)) + return o < bb + .read(def: VOffset.self, position: Int(vtable)) ? Int32(bb.read( + def: Int16.self, + position: Int(vtable + o))) : 0 + } + + /// Gets the indirect offset of the current stored object + /// (applicable only for object arrays) + /// - Parameter o: current offset + /// - Returns: offset of field within buffer + public func indirect(_ o: Int32) -> Int32 { + o + bb.read(def: Int32.self, position: Int(o)) + } + + /// String reads from the buffer with respect to position of the current table. + /// - Parameter offset: Offset of the string + public func string(at offset: Int32) -> String? { + directString(at: offset + position) + } + + /// Direct string reads from the buffer disregarding the position of the table. + /// It would be preferable to use string unless the current position of the table + /// is not needed + /// - Parameter offset: Offset of the string + public func directString(at offset: Int32) -> String? { + var offset = offset + offset += bb.read(def: Int32.self, position: Int(offset)) + let count = bb.read(def: Int32.self, position: Int(offset)) + let position = Int(offset) + MemoryLayout.size + return bb.readString(at: position, count: Int(count)) + } + + /// Reads from the buffer with respect to the position in the table. + /// - Parameters: + /// - type: Type of Element that needs to be read from the buffer + /// - o: Offset of the Element + public func readBuffer(of type: T.Type, at o: Int32) -> T { + directRead(of: T.self, offset: o + position) + } + + /// Reads from the buffer disregarding the position of the table. + /// It would be used when reading from an + /// ``` + /// let offset = __t.offset(10) + /// //Only used when the we already know what is the + /// // position in the table since __t.vector(at:) + /// // returns the index with respect to the position + /// __t.directRead(of: Byte.self, + /// offset: __t.vector(at: offset) + index * 1) + /// ``` + /// - Parameters: + /// - type: Type of Element that needs to be read from the buffer + /// - o: Offset of the Element + public func directRead(of type: T.Type, offset o: Int32) -> T { + let r = bb.read(def: T.self, position: Int(o)) + return r + } + + /// Returns that current `Union` object at a specific offset + /// by adding offset to the current position of table + /// - Parameter o: offset + /// - Returns: A flatbuffers object + public func union(_ o: Int32) -> T { + let o = o + position + return directUnion(o) + } + + /// Returns a direct `Union` object at a specific offset + /// - Parameter o: offset + /// - Returns: A flatbuffers object + public func directUnion(_ o: Int32) -> T { + T.init(bb, o: o + bb.read(def: Int32.self, position: Int(o))) + } + + /// Returns a vector of type T at a specific offset + /// This should only be used by `Scalars` + /// - Parameter off: Readable offset + /// - Returns: Returns a vector of type [T] + public func getVector(at off: Int32) -> [T]? { + let o = offset(off) + guard o != 0 else { return nil } + return bb.readSlice(index: Int(vector(at: o)), count: Int(vector(count: o))) + } + + /// Vector count gets the count of Elements within the array + /// - Parameter o: start offset of the vector + /// - returns: Count of elements + public func vector(count o: Int32) -> Int32 { + var o = o + o += position + o += bb.read(def: Int32.self, position: Int(o)) + return bb.read(def: Int32.self, position: Int(o)) + } + + /// Vector start index in the buffer + /// - Parameter o:start offset of the vector + /// - returns: the start index of the vector + public func vector(at o: Int32) -> Int32 { + var o = o + o += position + return o + bb.read(def: Int32.self, position: Int(o)) + 4 + } + + /// Reading an indirect offset of a table. + /// - Parameters: + /// - o: position within the buffer + /// - fbb: ByteBuffer + /// - Returns: table offset + static public func indirect(_ o: Int32, _ fbb: ByteBuffer) -> Int32 { + o + fbb.read(def: Int32.self, position: Int(o)) + } + + /// Gets a vtable value according to an table Offset and a field offset + /// - Parameters: + /// - o: offset relative to entire buffer + /// - vOffset: Field offset within a vtable + /// - fbb: ByteBuffer + /// - Returns: an position of a field + static public func offset( + _ o: Int32, + vOffset: Int32, + fbb: ByteBuffer) -> Int32 + { + let vTable = Int32(fbb.capacity) - o + return vTable + Int32(fbb.read( + def: Int16.self, + position: Int(vTable + vOffset - fbb.read( + def: Int32.self, + position: Int(vTable))))) + } + + /// Compares two objects at offset A and offset B within a ByteBuffer + /// - Parameters: + /// - off1: first offset to compare + /// - off2: second offset to compare + /// - fbb: Bytebuffer + /// - Returns: returns the difference between + static public func compare( + _ off1: Int32, + _ off2: Int32, + fbb: ByteBuffer) -> Int32 + { + let memorySize = Int32(MemoryLayout.size) + let _off1 = off1 + fbb.read(def: Int32.self, position: Int(off1)) + let _off2 = off2 + fbb.read(def: Int32.self, position: Int(off2)) + let len1 = fbb.read(def: Int32.self, position: Int(_off1)) + let len2 = fbb.read(def: Int32.self, position: Int(_off2)) + let startPos1 = _off1 + memorySize + let startPos2 = _off2 + memorySize + let minValue = min(len1, len2) + for i in 0...minValue { + let b1 = fbb.read(def: Int8.self, position: Int(i + startPos1)) + let b2 = fbb.read(def: Int8.self, position: Int(i + startPos2)) + if b1 != b2 { + return Int32(b2 - b1) + } + } + return len1 - len2 + } + + /// Compares two objects at offset A and array of `Bytes` within a ByteBuffer + /// - Parameters: + /// - off1: Offset to compare to + /// - key: bytes array to compare to + /// - fbb: Bytebuffer + /// - Returns: returns the difference between + static public func compare( + _ off1: Int32, + _ key: [Byte], + fbb: ByteBuffer) -> Int32 + { + let memorySize = Int32(MemoryLayout.size) + let _off1 = off1 + fbb.read(def: Int32.self, position: Int(off1)) + let len1 = fbb.read(def: Int32.self, position: Int(_off1)) + let len2 = Int32(key.count) + let startPos1 = _off1 + memorySize + let minValue = min(len1, len2) + for i in 0.. Int? { + if field >= _vtableLength { + return nil + } + + /// Reading the offset for the field needs to be read. + let offset: VOffset = try _verifier.getValue( + at: Int(clamping: _vtable &+ Int(field))) + + if offset > 0 { + return Int(clamping: _position &+ Int(offset)) + } + return nil + } + + /// Visits all the fields within the table to validate the integrity + /// of the data + /// - Parameters: + /// - field: voffset of the current field to be read + /// - fieldName: fieldname to report data Errors. + /// - required: If the field has to be available in the buffer + /// - type: Type of field to be read + /// - Throws: A `FlatbuffersErrors` where the field is corrupt + public mutating func visit( + field: VOffset, + fieldName: String, + required: Bool, + type: T.Type) throws where T: Verifiable + { + let derefValue = try dereference(field) + + if let value = derefValue { + try T.verify(&_verifier, at: value, of: T.self) + return + } + if required { + throw FlatbuffersErrors.requiredFieldDoesntExist( + position: field, + name: fieldName) + } + } + + /// Visits all the fields for a union object within the table to + /// validate the integrity of the data + /// - Parameters: + /// - key: Current Key Voffset + /// - field: Current field Voffset + /// - unionKeyName: Union key name + /// - fieldName: Field key name + /// - required: indicates if an object is required to be present + /// - completion: Completion is a handler that WILL be called in the generated + /// - Throws: A `FlatbuffersErrors` where the field is corrupt + public mutating func visit( + unionKey key: VOffset, + unionField field: VOffset, + unionKeyName: String, + fieldName: String, + required: Bool, + completion: @escaping (inout Verifier, T, Int) throws -> Void) throws + where T: UnionEnum + { + let keyPos = try dereference(key) + let valPos = try dereference(field) + + if keyPos == nil && valPos == nil { + if required { + throw FlatbuffersErrors.requiredFieldDoesntExist( + position: key, + name: unionKeyName) + } + return + } + + if let _key = keyPos, + let _val = valPos + { + /// verifiying that the key is within the buffer + try T.T.verify(&_verifier, at: _key, of: T.T.self) + guard let _enum = try T.init(value: _verifier._buffer.read( + def: T.T.self, + position: _key)) else + { + throw FlatbuffersErrors.unknownUnionCase + } + /// we are assuming that Unions will always be of type Uint8 + try completion( + &_verifier, + _enum, + _val) + return + } + throw FlatbuffersErrors.valueNotFound( + key: keyPos, + keyName: unionKeyName, + field: valPos, + fieldName: fieldName) + } + + /// Visits and validates all the objects within a union vector + /// - Parameters: + /// - key: Current Key Voffset + /// - field: Current field Voffset + /// - unionKeyName: Union key name + /// - fieldName: Field key name + /// - required: indicates if an object is required to be present + /// - completion: Completion is a handler that WILL be called in the generated + /// - Throws: A `FlatbuffersErrors` where the field is corrupt + public mutating func visitUnionVector( + unionKey key: VOffset, + unionField field: VOffset, + unionKeyName: String, + fieldName: String, + required: Bool, + completion: @escaping (inout Verifier, T, Int) throws -> Void) throws + where T: UnionEnum + { + let keyVectorPosition = try dereference(key) + let offsetVectorPosition = try dereference(field) + + if let keyPos = keyVectorPosition, + let valPos = offsetVectorPosition + { + try UnionVector.verify( + &_verifier, + keyPosition: keyPos, + fieldPosition: valPos, + unionKeyName: unionKeyName, + fieldName: fieldName, + completion: completion) + return + } + if required { + throw FlatbuffersErrors.requiredFieldDoesntExist( + position: field, + name: fieldName) + } + } + + /// Finishs the current Table verifier, and subtracts the current + /// table from the incremented depth. + public mutating func finish() { + _verifier.finish() + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/VeriferOptions.swift b/submodules/TelegramCore/FlatBuffers/Sources/VeriferOptions.swift new file mode 100644 index 0000000000..a7f11e243d --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/VeriferOptions.swift @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// `VerifierOptions` is a set of options to verify a flatbuffer +public struct VerifierOptions { + + /// Maximum `Apparent` size if the buffer can be expanded into a DAG tree + internal var _maxApparentSize: UOffset + + /// Maximum table count allowed in a buffer + internal var _maxTableCount: UOffset + + /// Maximum depth allowed in a buffer + internal var _maxDepth: UOffset + + /// Ignoring missing null terminals in strings + internal var _ignoreMissingNullTerminators: Bool + + /// initializes the set of options for the verifier + /// - Parameters: + /// - maxDepth: Maximum depth allowed in a buffer + /// - maxTableCount: Maximum table count allowed in a buffer + /// - maxApparentSize: Maximum `Apparent` size if the buffer can be expanded into a DAG tree + /// - ignoreMissingNullTerminators: Ignoring missing null terminals in strings *Currently not supported in swift* + public init( + maxDepth: UOffset = 64, + maxTableCount: UOffset = 1000000, + maxApparentSize: UOffset = 1 << 31, + ignoreMissingNullTerminators: Bool = false) + { + _maxDepth = maxDepth + _maxTableCount = maxTableCount + _maxApparentSize = maxApparentSize + _ignoreMissingNullTerminators = ignoreMissingNullTerminators + } + +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Verifiable.swift b/submodules/TelegramCore/FlatBuffers/Sources/Verifiable.swift new file mode 100644 index 0000000000..3d3e08f0f8 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Verifiable.swift @@ -0,0 +1,213 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Verifiable is a protocol all swift flatbuffers object should conform to, +/// since swift is similar to `cpp` and `rust` where the data is read directly +/// from `unsafeMemory` thus the need to verify if the buffer received is a valid one +public protocol Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable +} + +extension Verifiable { + + /// Verifies if the current range to be read is within the bounds of the buffer, + /// and if the range is properly aligned + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Erros thrown from `isAligned` & `rangeInBuffer` + /// - Returns: a tuple of the start position and the count of objects within the range + @discardableResult + public static func verifyRange( + _ verifier: inout Verifier, + at position: Int, of type: T.Type) throws -> (start: Int, count: Int) + { + let len: UOffset = try verifier.getValue(at: position) + let intLen = Int(len) + let start = Int(clamping: (position &+ MemoryLayout.size).magnitude) + try verifier.isAligned(position: start, type: type.self) + try verifier.rangeInBuffer(position: start, size: intLen) + return (start, intLen) + } +} + +extension Verifiable where Self: Scalar { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + try verifier.inBuffer(position: position, of: type.self) + } +} + +// MARK: - ForwardOffset + +/// ForwardOffset is a container to wrap around the Generic type to be verified +/// from the flatbuffers object. +public enum ForwardOffset: Verifiable where U: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + let offset: UOffset = try verifier.getValue(at: position) + let nextOffset = Int(clamping: (Int(offset) &+ position).magnitude) + try U.verify(&verifier, at: nextOffset, of: U.self) + } +} + +// MARK: - Vector + +/// Vector is a container to wrap around the Generic type to be verified +/// from the flatbuffers object. +public enum Vector: Verifiable where U: Verifiable, S: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + /// checks if the next verification type S is equal to U of type forwardOffset + /// This had to be done since I couldnt find a solution for duplicate call functions + /// A fix will be appreciated + if U.self is ForwardOffset.Type { + let range = try verifyRange(&verifier, at: position, of: UOffset.self) + for index in stride( + from: range.start, + to: Int( + clamping: range + .start &+ (range.count &* MemoryLayout.size)), + by: MemoryLayout.size) + { + try U.verify(&verifier, at: index, of: U.self) + } + } else { + try S.verifyRange(&verifier, at: position, of: S.self) + } + } +} + +// MARK: - UnionVector + +/// UnionVector is a container to wrap around the Generic type to be verified +/// from the flatbuffers object. +public enum UnionVector where S: UnionEnum { + + /// Completion handler for the function Verify, that passes the verifier + /// enum type and position of union field + public typealias Completion = (inout Verifier, S, Int) throws -> Void + + /// Verifies if the current range to be read is within the bounds of the buffer, + /// and if the range is properly aligned. It also verifies if the union type is a + /// *valid/supported* union type. + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - keyPosition: Current union key position within the buffer + /// - fieldPosition: Current union field position within the buffer + /// - unionKeyName: Name of key to written if error is presented + /// - fieldName: Name of field to written if error is presented + /// - completion: Completion is a handler that WILL be called in the generated + /// code to verify the actual objects + /// - Throws: FlatbuffersErrors + public static func verify( + _ verifier: inout Verifier, + keyPosition: Int, + fieldPosition: Int, + unionKeyName: String, + fieldName: String, + completion: @escaping Completion) throws + { + /// Get offset for union key vectors and offset vectors + let keyOffset: UOffset = try verifier.getValue(at: keyPosition) + let fieldOffset: UOffset = try verifier.getValue(at: fieldPosition) + + /// Check if values are within the buffer, returns the start position of vectors, and vector counts + /// Using &+ is safe since we already verified that the value is within the buffer, where the max is + /// going to be 2Gib and swift supports Int64 by default + let keysRange = try S.T.verifyRange( + &verifier, + at: Int(keyOffset) &+ keyPosition, + of: S.T.self) + let offsetsRange = try UOffset.verifyRange( + &verifier, + at: Int(fieldOffset) &+ fieldPosition, + of: UOffset.self) + + guard keysRange.count == offsetsRange.count else { + throw FlatbuffersErrors.unionVectorSize( + keyVectorSize: keysRange.count, + fieldVectorSize: offsetsRange.count, + unionKeyName: unionKeyName, + fieldName: fieldName) + } + + var count = 0 + /// Iterate over the vector of keys and offsets. + while count < keysRange.count { + + /// index of readable enum value in array + let keysIndex = MemoryLayout.size * count + guard let _enum = try S.init(value: verifier._buffer.read( + def: S.T.self, + position: keysRange.start + keysIndex)) else + { + throw FlatbuffersErrors.unknownUnionCase + } + /// index of readable offset value in array + let fieldIndex = MemoryLayout.size * count + try completion(&verifier, _enum, offsetsRange.start + fieldIndex) + count += 1 + } + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Verifier.swift b/submodules/TelegramCore/FlatBuffers/Sources/Verifier.swift new file mode 100644 index 0000000000..0d52ccd8a8 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Verifier.swift @@ -0,0 +1,238 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Verifier that check if the buffer passed into it is a valid, +/// safe, aligned Flatbuffers object since swift read from `unsafeMemory` +public struct Verifier { + + /// Flag to check for alignment if true + fileprivate let _checkAlignment: Bool + /// Storage for all changing values within the verifier + private let storage: Storage + /// Current verifiable ByteBuffer + internal var _buffer: ByteBuffer + /// Options for verification + internal let _options: VerifierOptions + + /// Current stored capacity within the verifier + var capacity: Int { + storage.capacity + } + + /// Current depth of verifier + var depth: Int { + storage.depth + } + + /// Current table count + var tableCount: Int { + storage.tableCount + } + + + /// Initializer for the verifier + /// - Parameters: + /// - buffer: Bytebuffer that is required to be verified + /// - options: `VerifierOptions` that set the rule for some of the verification done + /// - checkAlignment: If alignment check is required to be preformed + /// - Throws: `exceedsMaxSizeAllowed` if capacity of the buffer is more than 2GiB + public init( + buffer: inout ByteBuffer, + options: VerifierOptions = .init(), + checkAlignment: Bool = true) throws + { + guard buffer.capacity < FlatBufferMaxSize else { + throw FlatbuffersErrors.exceedsMaxSizeAllowed + } + + _buffer = buffer + _checkAlignment = checkAlignment + _options = options + storage = Storage(capacity: buffer.capacity) + } + + /// Resets the verifier to initial state + public func reset() { + storage.depth = 0 + storage.tableCount = 0 + } + + /// Checks if the value of type `T` is aligned properly in the buffer + /// - Parameters: + /// - position: Current position + /// - type: Type of value to check + /// - Throws: `missAlignedPointer` if the pointer is not aligned properly + public func isAligned(position: Int, type: T.Type) throws { + + /// If check alignment is false this mutating function doesnt continue + if !_checkAlignment { return } + + /// advance pointer to position X + let ptr = _buffer._storage.memory.advanced(by: position) + /// Check if the pointer is aligned + if Int(bitPattern: ptr) & (MemoryLayout.alignment &- 1) == 0 { + return + } + + throw FlatbuffersErrors.missAlignedPointer( + position: position, + type: String(describing: T.self)) + } + + /// Checks if the value of Size "X" is within the range of the buffer + /// - Parameters: + /// - position: Current position to be read + /// - size: `Byte` Size of readable object within the buffer + /// - Throws: `outOfBounds` if the value is out of the bounds of the buffer + /// and `apparentSizeTooLarge` if the apparent size is bigger than the one specified + /// in `VerifierOptions` + public func rangeInBuffer(position: Int, size: Int) throws { + let end = UInt(clamping: (position &+ size).magnitude) + if end > _buffer.capacity { + throw FlatbuffersErrors.outOfBounds(position: end, end: storage.capacity) + } + storage.apparentSize = storage.apparentSize &+ UInt32(size) + if storage.apparentSize > _options._maxApparentSize { + throw FlatbuffersErrors.apparentSizeTooLarge + } + } + + /// Validates if a value of type `T` is aligned and within the bounds of + /// the buffer + /// - Parameters: + /// - position: Current readable position + /// - type: Type of value to check + /// - Throws: FlatbuffersErrors + public func inBuffer(position: Int, of type: T.Type) throws { + try isAligned(position: position, type: type) + try rangeInBuffer(position: position, size: MemoryLayout.size) + } + + /// Visits a table at the current position and validates if the table meets + /// the rules specified in the `VerifierOptions` + /// - Parameter position: Current position to be read + /// - Throws: FlatbuffersErrors + /// - Returns: A `TableVerifier` at the current readable table + public mutating func visitTable(at position: Int) throws -> TableVerifier { + let vtablePosition = try derefOffset(position: position) + let vtableLength: VOffset = try getValue(at: vtablePosition) + + let length = Int(vtableLength) + try isAligned( + position: Int(clamping: (vtablePosition + length).magnitude), + type: VOffset.self) + try rangeInBuffer(position: vtablePosition, size: length) + + storage.tableCount += 1 + + if storage.tableCount > _options._maxTableCount { + throw FlatbuffersErrors.maximumTables + } + + storage.depth += 1 + + if storage.depth > _options._maxDepth { + throw FlatbuffersErrors.maximumDepth + } + + return TableVerifier( + position: position, + vtable: vtablePosition, + vtableLength: length, + verifier: &self) + } + + /// Validates if a value of type `T` is within the buffer and returns it + /// - Parameter position: Current position to be read + /// - Throws: `inBuffer` errors + /// - Returns: a value of type `T` usually a `VTable` or a table offset + internal func getValue(at position: Int) throws -> T { + try inBuffer(position: position, of: T.self) + return _buffer.read(def: T.self, position: position) + } + + /// derefrences an offset within a vtable to get the position of the field + /// in the bytebuffer + /// - Parameter position: Current readable position + /// - Throws: `inBuffer` errors & `signedOffsetOutOfBounds` + /// - Returns: Current readable position for a field + @inline(__always) + internal func derefOffset(position: Int) throws -> Int { + try inBuffer(position: position, of: Int32.self) + + let offset = _buffer.read(def: Int32.self, position: position) + // switching to int32 since swift's default Int is int64 + // this should be safe since we already checked if its within + // the buffer + let _int32Position = UInt32(position) + + let reportedOverflow: (partialValue: UInt32, overflow: Bool) + if offset > 0 { + reportedOverflow = _int32Position + .subtractingReportingOverflow(offset.magnitude) + } else { + reportedOverflow = _int32Position + .addingReportingOverflow(offset.magnitude) + } + + /// since `subtractingReportingOverflow` & `addingReportingOverflow` returns true, + /// if there is overflow we return failure + if reportedOverflow.overflow || reportedOverflow.partialValue > _buffer + .capacity + { + throw FlatbuffersErrors.signedOffsetOutOfBounds( + offset: Int(offset), + position: position) + } + + return Int(reportedOverflow.partialValue) + } + + /// finishes the current iteration of verification on an object + internal func finish() { + storage.depth -= 1 + } + + @inline(__always) + func verify(id: String) throws { + let size = MemoryLayout.size + guard storage.capacity >= (size * 2) else { + throw FlatbuffersErrors.bufferDoesntContainID + } + let str = _buffer.readString(at: size, count: size) + if id == str { + return + } + throw FlatbuffersErrors.bufferIdDidntMatchPassedId + } + + final private class Storage { + /// Current ApparentSize + fileprivate var apparentSize: UOffset = 0 + /// Amount of tables present within a buffer + fileprivate var tableCount = 0 + /// Capacity of the current buffer + fileprivate let capacity: Int + /// Current reached depth within the buffer + fileprivate var depth = 0 + + init(capacity: Int) { + self.capacity = capacity + } + } +} diff --git a/submodules/TelegramCore/FlatSerialization/BUILD b/submodules/TelegramCore/FlatSerialization/BUILD new file mode 100644 index 0000000000..e18b8eb401 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/BUILD @@ -0,0 +1,56 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +models = glob([ + "Models/*.fbs", +]) + +model_names = [ + f[7:-4] for f in models +] + +generated_models = [ "{}_generated.swift".format(name) for name in model_names ] +flatc_input = " ".join([ "$(location Models/{}.fbs)".format(name) for name in model_names ]) + +genrule( + name = "GenerateModels", + srcs = models, + tools = [ + "//third-party/flatc:flatc_bin" + ], + cmd_bash = + """ + set -ex + FLATC="$$(pwd)/$(location //third-party/flatc:flatc_bin)" + + BUILD_DIR="$(RULEDIR)/build" + rm -rf "$$BUILD_DIR" + mkdir -p "$$BUILD_DIR" + + "$$FLATC" --swift -o "$$BUILD_DIR" {flatc_input} + """.format( + flatc_input=flatc_input + ) + "\n" + "\n".join([ + """ + cp "$$BUILD_DIR/{name}_generated.swift" "$(location {name}_generated.swift)" + """.format(name=name) for name in model_names + ]), + outs = generated_models, + visibility = [ + "//visibility:public", + ] +) + +swift_library( + name = "FlatSerialization", + module_name = "FlatSerialization", + srcs = generated_models, + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/TelegramCore/FlatBuffers", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramCore/FlatSerialization/Models/MediaId.fbs b/submodules/TelegramCore/FlatSerialization/Models/MediaId.fbs new file mode 100644 index 0000000000..5792a89655 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/MediaId.fbs @@ -0,0 +1,6 @@ +namespace TelegramCore; + +struct MediaId { + namespace: int; + id: int64; +} diff --git a/submodules/TelegramCore/FlatSerialization/Models/PixelDimensions.fbs b/submodules/TelegramCore/FlatSerialization/Models/PixelDimensions.fbs new file mode 100644 index 0000000000..dc1be6bcc0 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/PixelDimensions.fbs @@ -0,0 +1,6 @@ +namespace TelegramCore; + +struct PixelDimensions { + width: int; + height: int; +} diff --git a/submodules/TelegramCore/FlatSerialization/Models/TelegramMediaFile.fbs b/submodules/TelegramCore/FlatSerialization/Models/TelegramMediaFile.fbs new file mode 100644 index 0000000000..0eefc28bfb --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/TelegramMediaFile.fbs @@ -0,0 +1,11 @@ +include "MediaId.fbs"; +include "VideoThumbnail.fbs"; + +namespace TelegramCore; + +table TelegramMediaFile { + id: MediaId; + videoThumbnails: [VideoThumbnail]; +} + +root_type TelegramMediaFile; diff --git a/submodules/TelegramCore/FlatSerialization/Models/VideoThumbnail.fbs b/submodules/TelegramCore/FlatSerialization/Models/VideoThumbnail.fbs new file mode 100644 index 0000000000..62a84090f9 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/VideoThumbnail.fbs @@ -0,0 +1,7 @@ +include "PixelDimensions.fbs"; + +namespace TelegramCore; + +struct VideoThumbnail { + dimensions: PixelDimensions; +} diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index e2ef92c5d0..3714cf4388 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -205,6 +205,7 @@ private var declaredEncodables: Void = { declareEncodable(WallpaperDataResource.self, f: { WallpaperDataResource(decoder: $0) }) declareEncodable(ForwardOptionsMessageAttribute.self, f: { ForwardOptionsMessageAttribute(decoder: $0) }) declareEncodable(SendAsMessageAttribute.self, f: { SendAsMessageAttribute(decoder: $0) }) + declareEncodable(ForwardVideoTimestampAttribute.self, f: { ForwardVideoTimestampAttribute(decoder: $0) }) declareEncodable(AudioTranscriptionMessageAttribute.self, f: { AudioTranscriptionMessageAttribute(decoder: $0) }) declareEncodable(NonPremiumMessageAttribute.self, f: { NonPremiumMessageAttribute(decoder: $0) }) declareEncodable(TelegramExtendedMedia.self, f: { TelegramExtendedMedia(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index b9286941eb..020ea46c98 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -326,35 +326,34 @@ struct ParsedMessageWebpageAttributes { var isSafe: Bool } -func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?) { +func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?, videoTimestamp: Int32?) { if let media = media { switch media { case let .messageMediaPhoto(flags, photo, ttlSeconds): if let photo = photo { if let mediaImage = telegramMediaImageFromApiPhoto(photo) { - return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil) + return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil, nil) } } else { - return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil) + return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil, nil) } case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId): let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard) - return (mediaContact, nil, nil, nil, nil) + return (mediaContact, nil, nil, nil, nil, nil) case let .messageMediaGeo(geo): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil, nil) case let .messageMediaVenue(geo, title, address, provider, venueId, venueType): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil, nil) case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) - return (mediaMap, nil, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil, nil) case let .messageMediaDocument(flags, document, altDocuments, coverPhoto, videoTimestamp, ttlSeconds): - let _ = videoTimestamp if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments, videoCover: coverPhoto) { - return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) + return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil, videoTimestamp) } } else { var data: TelegramMediaExpiredContentData @@ -365,7 +364,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { data = .file } - return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil) + return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil, nil) } case let .messageMediaWebPage(flags, webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage) { @@ -380,14 +379,14 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI forceLargeMedia: webpageForceLargeMedia, isManuallyAdded: (flags & (1 << 3)) != 0, isSafe: (flags & (1 << 4)) != 0 - )) + ), nil) } case .messageMediaUnsupported: - return (TelegramMediaUnsupported(), nil, nil, nil, nil) + return (TelegramMediaUnsupported(), nil, nil, nil, nil, nil) case .messageMediaEmpty: break case let .messageMediaGame(game): - return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil) + return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil, nil) case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia): var parsedFlags = TelegramMediaInvoiceFlags() if (flags & (1 << 3)) != 0 { @@ -396,7 +395,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI if (flags & (1 << 1)) != 0 { parsedFlags.insert(.shippingAddressRequested) } - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): @@ -421,13 +420,13 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI questionEntities = messageTextEntitiesFromApiEntities(entities) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil, nil) } case let .messageMediaDice(value, emoticon): - return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil) + return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil) case let .messageMediaStory(flags, peerId, id, _): let isMention = (flags & (1 << 1)) != 0 - return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil) + return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil, nil) case let .messageMediaGiveaway(apiFlags, channels, countries, prizeDescription, quantity, months, stars, untilDate): var flags: TelegramMediaGiveaway.Flags = [] if (apiFlags & (1 << 0)) != 0 { @@ -439,9 +438,9 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else if let stars { prize = .stars(amount: stars) } else { - return (nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, nil) } - return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) + return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil) case let .messageMediaGiveawayResults(apiFlags, channelId, additionalPeersCount, launchMsgId, winnersCount, unclaimedCount, winners, months, stars, prizeDescription, untilDate): var flags: TelegramMediaGiveawayResults.Flags = [] if (apiFlags & (1 << 0)) != 0 { @@ -456,15 +455,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else if let stars { prize = .stars(amount: stars) } else { - return (nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, nil) } - return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) + return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil) case let .messageMediaPaidMedia(starsAmount, apiExtendedMedia): - return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil) + return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil, nil) } } - return (nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, nil) } func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { @@ -811,7 +810,7 @@ extension StoreMessage { var consumableContent: (Bool, Bool)? = nil if let media = media { - let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes, videoTimestamp) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) @@ -828,6 +827,10 @@ extension StoreMessage { attributes.append(MediaSpoilerMessageAttribute()) } + if let videoTimestamp { + attributes.append(ForwardVideoTimestampAttribute(timestamp: videoTimestamp)) + } + if mediaValue is TelegramMediaWebpage { let leadingPreview = (flags & (1 << 27)) != 0 diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift index e82718a39b..4a6200ae4b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift @@ -16,8 +16,7 @@ extension TelegramExtendedMedia { } self = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) - if let media = media { + if let media = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId).media { self = .full(media: media) } else { return nil diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index a5ac46ba0b..3f636dc161 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -252,6 +252,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as EffectMessageAttribute: return true + case _ as ForwardVideoTimestampAttribute: + return true default: return false } @@ -950,6 +952,12 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, forwardInfo = nil } } + + for attribute in requestedAttributes { + if attribute is ForwardVideoTimestampAttribute { + attributes.append(attribute) + } + } } else { attributes.append(contentsOf: filterMessageAttributesForOutgoingMessage(sourceMessage.attributes)) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 1cf0ee08d4..8522225221 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -229,7 +229,19 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } |> mapToSignal { validatedResource -> Signal in if let validatedResource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = validatedResource.fileReference { - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: 0, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, videoTimestamp: nil, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) + var flags: Int32 = 0 + + var videoTimestamp: Int32? + for attribute in attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp + } + } + if videoTimestamp != nil { + flags |= 1 << 4 + } + + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, videoTimestamp: videoTimestamp, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) } else { return .fail(.generic) } @@ -239,14 +251,18 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post var flags: Int32 = 0 var emojiSearchQuery: String? + var videoTimestamp: Int32? for attribute in attributes { if let attribute = attribute as? EmojiSearchQueryMessageAttribute { emojiSearchQuery = attribute.query flags |= (1 << 1) + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + flags |= (1 << 4) + videoTimestamp = attribute.timestamp } } - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, videoTimestamp: nil, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, videoTimestamp: videoTimestamp, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: false, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) @@ -851,7 +867,6 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili switch result { case let .media(media, key): if !forceReupload, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { - var videoCoverSignal: Signal = .single(.none) if let cover = file.videoCover, let resource = cover.representations.first?.resource { let fileReference: AnyMediaReference @@ -875,6 +890,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapToSignal { videoCover -> Signal in var flags: Int32 = 0 var ttlSeconds: Int32? + var videoTimestamp: Int32? if let autoclearMessageAttribute = autoclearMessageAttribute { flags |= 1 << 0 ttlSeconds = autoclearMessageAttribute.timeout @@ -883,6 +899,9 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili for attribute in attributes { if let _ = attribute as? MediaSpoilerMessageAttribute { flags |= 1 << 2 + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + flags |= (1 << 4) + videoTimestamp = attribute.timestamp } } @@ -896,7 +915,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili return .single(.progress(PendingMessageUploadedContentProgress(progress: 1.0))) |> then( - .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) + .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) ) } } @@ -1117,6 +1136,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } var ttlSeconds: Int32? + var videoTimestamp: Int32? for attribute in attributes { if let attribute = attribute as? AutoclearTimeoutMessageAttribute { flags |= 1 << 1 @@ -1124,6 +1144,8 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } else if let _ = attribute as? MediaSpoilerMessageAttribute { flags |= 1 << 5 hasSpoiler = true + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp } } @@ -1152,12 +1174,16 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } } - if ttlSeconds != nil { - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey))) + if videoTimestamp != nil { + flags |= 1 << 7 + } + + if ttlSeconds != nil { + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey))) } if !isGrouped { - let resultInfo = PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey) + let resultInfo = PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey) return .single(.content(resultInfo)) } @@ -1168,7 +1194,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapError { _ -> PendingMessageUploadError in } |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { - return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds))) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { @@ -1186,8 +1212,11 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili if let _ = videoCoverPhoto { flags |= (1 << 3) } + if videoTimestamp != nil { + flags |= (1 << 4) + } - let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) + let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) if let _ = ttlSeconds { return .single(result) } else { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 0352b97142..3532126d7c 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -324,6 +324,7 @@ private func sendUploadedMessageContent( } var replyToStoryId: StoryId? var scheduleTime: Int32? + var videoTimestamp: Int32? var sendAsPeerId: PeerId? var bubbleUpEmojiOrStickersets = false @@ -360,6 +361,9 @@ private func sendUploadedMessageContent( scheduleTime = attribute.scheduleTime } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + flags |= Int32(1 << 20) + videoTimestamp = attribute.timestamp } } @@ -442,7 +446,7 @@ private func sendUploadedMessageContent( } if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index b5d80567dc..8bea175a4a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1159,7 +1159,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: let messageText = text var medias: [Media] = [] - let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes, videoTimestamp) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) @@ -1172,6 +1172,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let expirationTimer = expirationTimer { attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) } + if let videoTimestamp { + attributes.append(ForwardVideoTimestampAttribute(timestamp: videoTimestamp)) + } if let nonPremium = nonPremium, nonPremium { attributes.append(NonPremiumMessageAttribute()) diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 0fe4702964..5f112f6c70 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -152,7 +152,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes forwardInfo = updatedMessage.forwardInfo threadId = updatedMessage.threadId } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { - let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (mediaValue, _, nonPremium, hasSpoiler, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { media = [mediaValue] } else { diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 65fe389de6..c21584858e 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -1005,8 +1005,11 @@ private final class CallSessionManagerContext { if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { switch context.state { - case .accepting, .active, .dropping, .requesting, .ringing, .terminated, .requested, .switchedToConference: + case .accepting, .dropping, .requesting, .ringing, .terminated, .requested, .switchedToConference: break + case let .active(id, accessHash, beginTimestamp, key, keyId, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, _): + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: beginTimestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) + self.contextUpdated(internalId: internalId) case let .awaitingConfirmation(_, accessHash, gAHash, b, config): if let (key, calculatedKeyId, keyVisualHash) = self.makeSessionEncryptionKey(config: config, gAHash: gAHash, b: b, gA: gAOrB.makeData()) { if keyFingerprint == calculatedKeyId { diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index b15e196681..6765dda05b 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -822,6 +822,7 @@ public final class PendingMessageManager { var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? + var videoTimestamp: Int32? var sendAsPeerId: PeerId? var quickReply: OutgoingQuickReplyMessageAttribute? var messageEffect: EffectMessageAttribute? @@ -859,6 +860,8 @@ public final class PendingMessageManager { messageEffect = attribute } else if let _ = attribute as? InvertMediaMessageAttribute { flags |= Int32(1 << 16) + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp } } @@ -873,6 +876,9 @@ public final class PendingMessageManager { if hideCaptions { flags |= (1 << 12) } + if videoTimestamp != nil { + flags |= Int32(1 << 20) + } var sendAsInputPeer: Api.InputPeer? if let sendAsPeerId = sendAsPeerId, let sendAsPeer = transaction.getPeer(sendAsPeerId), let inputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: accountPeerId) { @@ -926,7 +932,7 @@ public final class PendingMessageManager { } else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) { let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id) - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp), tag: dependencyTag) } else { assertionFailure() sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "Invalid forward source")) @@ -1268,6 +1274,7 @@ public final class PendingMessageManager { var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? + var videoTimestamp: Int32? var sendAsPeerId: PeerId? var bubbleUpEmojiOrStickersets = false var quickReply: OutgoingQuickReplyMessageAttribute? @@ -1310,6 +1317,8 @@ public final class PendingMessageManager { quickReply = attribute } else if let attribute = attribute as? EffectMessageAttribute { messageEffect = attribute + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp } } @@ -1520,8 +1529,12 @@ public final class PendingMessageManager { flags |= 1 << 17 } + if videoTimestamp != nil { + flags |= 1 << 20 + } + if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ForwardVideoTimestampAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ForwardVideoTimestampAttribute.swift new file mode 100644 index 0000000000..f23737d7b9 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ForwardVideoTimestampAttribute.swift @@ -0,0 +1,18 @@ +import Foundation +import Postbox + +public class ForwardVideoTimestampAttribute: MessageAttribute { + public let timestamp: Int32 + + public init(timestamp: Int32) { + self.timestamp = timestamp + } + + required public init(decoder: PostboxDecoder) { + self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.timestamp, forKey: "timestamp") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalMediaPlaybackInfoAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalMediaPlaybackInfoAttribute.swift new file mode 100644 index 0000000000..ecc83d4a59 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalMediaPlaybackInfoAttribute.swift @@ -0,0 +1,18 @@ +import Foundation +import Postbox + +public class LocalMediaPlaybackInfoAttribute: MessageAttribute { + public let data: Data + + public init(data: Data) { + self.data = data + } + + required public init(decoder: PostboxDecoder) { + self.data = decoder.decodeDataForKey("d") ?? Data() + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeData(self.data, forKey: "d") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 2ce6c8d17a..f19036ecf6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -140,6 +140,7 @@ public struct Namespaces { public static let cachedPremiumGiftCodeOptions: Int8 = 42 public static let cachedProfileGifts: Int8 = 43 public static let recommendedBots: Int8 = 44 + public static let channelsForPublicReaction: Int8 = 45 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index 94bc0cd63d..bb0bc2a6d4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -113,10 +113,10 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { break } } - var derivedData: DerivedDataMessageAttribute? + var previousDerivedData: DerivedDataMessageAttribute? for attribute in previous { if let attribute = attribute as? DerivedDataMessageAttribute { - derivedData = attribute + previousDerivedData = attribute break } } @@ -134,17 +134,16 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { updated.append(audioTranscription) } } - if let derivedData = derivedData { + if let previousDerivedData { var found = false for i in 0 ..< updated.count { - if let attribute = updated[i] as? DerivedDataMessageAttribute { - updated[i] = derivedData + if let _ = updated[i] as? DerivedDataMessageAttribute { found = true break } } if !found { - updated.append(derivedData) + updated.append(previousDerivedData) } } }, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index b7d94d67dc..426de8e7a8 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -1,4 +1,4 @@ - import Foundation +import Foundation import Postbox private let typeFileName: Int32 = 0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index e085be0fa4..9001c25b2d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -1117,10 +1117,15 @@ public final class GroupCallParticipantsContext { var pendingMuteStateChanges: [PeerId: MuteStateChange] = [:] + var hasLocalVideo: PeerId? = nil + var isEmpty: Bool { if !self.pendingMuteStateChanges.isEmpty { return false } + if self.hasLocalVideo != nil { + return false + } return true } } @@ -1254,6 +1259,12 @@ public final class GroupCallParticipantsContext { publicState.participants[i].raiseHandRating = nil sortAgain = true } + + if let hasLocalVideoPeerId = state.overlayState.hasLocalVideo, hasLocalVideoPeerId == publicState.participants[i].peer.id { + if publicState.participants[i].videoDescription == nil { + publicState.participants[i].videoDescription = GroupCallParticipantsContext.Participant.VideoDescription(endpointId: "_local", ssrcGroups: [], audioSsrc: nil, isPaused: false) + } + } } if sortAgain { publicState.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: publicState.sortAscending) }) @@ -1943,6 +1954,10 @@ public final class GroupCallParticipantsContext { self.localIsVideoPaused = isVideoPaused self.localIsPresentationPaused = isPresentationPaused + if let isVideoMuted { + self.stateValue.overlayState.hasLocalVideo = isVideoMuted ? nil : peerId + } + let disposable = MetaDisposable() let account = self.account diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 53c4260259..2a7532b66b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -753,9 +753,12 @@ public extension TelegramEngine.EngineData.Item { } if let cachedData = view.cachedPeerData as? CachedUserData { return cachedData.starGiftsCount - } else { - return nil } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.starGiftsCount + } + return nil + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 7a32432996..5e69125e09 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -468,7 +468,7 @@ private class AdMessagesHistoryContextImpl { } let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) } - let (contentMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let contentMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media parsedMessages.append(CachedMessage( opaqueId: randomId.makeData(), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 3176bcd769..cfaf228f4f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1250,7 +1250,7 @@ func _internal_uploadStoryImpl( } id = idValue - let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId) + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, toPeerId).media if let parsedMedia = parsedMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile) } @@ -1593,7 +1593,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng if case let .updateStory(_, story) = update { switch story { case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _): - let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, account.peerId).media if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false, skipPreviews: updatingCoverTime) } @@ -2018,7 +2018,7 @@ extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { case let .storyItem(flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction): - let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? if let privacy = privacy { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 1973674532..83d0119ae9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -589,6 +589,28 @@ public extension TelegramEngine { |> ignoreValues } + public func updateLocallyDerivedData(messageId: MessageId, update: @escaping ([String: CodableEntry]) -> [String: CodableEntry]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + transaction.updateMessage(messageId, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var attributes = currentMessage.attributes + var data: [String: CodableEntry] = [:] + if let index = attributes.firstIndex(where: { $0 is DerivedDataMessageAttribute }) { + data = (attributes[index] as? DerivedDataMessageAttribute)?.data ?? [:] + attributes.remove(at: index) + } + data = update(data) + + if !data.isEmpty { + attributes.append(DerivedDataMessageAttribute(data: data)) + } + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + |> ignoreValues + } + public func rateAudioTranscription(messageId: MessageId, id: Int64, isGood: Bool) -> Signal { return _internal_rateAudioTranscription(postbox: self.account.postbox, network: self.account.network, messageId: messageId, id: id, isGood: isGood) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 24b3cd52a9..99e2688125 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -970,6 +970,12 @@ private final class ProfileGiftsContextImpl { self.actionDisposable.dispose() } + func reload() { + gifts = [] + dataState = .ready(canLoadMore: true, nextOffset: nil) + self.loadMore() + } + func loadMore() { let peerId = self.peerId let accountPeerId = self.account.peerId @@ -1429,6 +1435,12 @@ public final class ProfileGiftsContext { } } + public func reload() { + self.impl.with { impl in + impl.reload() + } + } + public func updateStarGiftAddedToProfile(reference: StarGiftReference, added: Bool) { self.impl.with { impl in impl.updateStarGiftAddedToProfile(reference: reference, added: added) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 7d54e95925..233b09824f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -583,21 +583,25 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann final class CachedStorySendAsPeers: Codable { public let peerIds: [PeerId] + public let timestamp: Double - public init(peerIds: [PeerId]) { + public init(peerIds: [PeerId], timestamp: Double) { self.peerIds = peerIds + self.timestamp = timestamp } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) self.peerIds = try container.decode([Int64].self, forKey: "l").map(PeerId.init) + self.timestamp = try container.decodeIfPresent(Double.self, forKey: "ts") ?? 0.0 } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l") + try container.encode(self.timestamp, forKey: "ts") } } @@ -644,7 +648,7 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { } } - if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id))) { + if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id), timestamp: CFAbsoluteTimeGetCurrent())) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)), entry: entry) } @@ -660,6 +664,77 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { } } +func _internal_channelsForPublicReaction(account: Account, useLocalCache: Bool) -> Signal<[Peer], NoError> { + let accountPeerId = account.peerId + return account.postbox.transaction { transaction -> ([Peer], Double)? in + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.channelsForPublicReaction, key: ValueBoxKey(length: 0)))?.get(CachedStorySendAsPeers.self) { + return (entry.peerIds.compactMap(transaction.getPeer), entry.timestamp) + } else { + return nil + } + } + |> mapToSignal { cachedPeers in + let remote: Signal<[Peer], NoError> = account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: 0)) + |> retryRequest + |> mapToSignal { result -> Signal<[Peer], NoError> in + return account.postbox.transaction { transaction -> [Peer] in + let chats: [Api.Chat] + let parsedPeers: AccumulatedPeers + switch result { + case let .chats(apiChats): + chats = apiChats + case let .chatsSlice(_, apiChats): + chats = apiChats + } + parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + var peers: [Peer] = [] + for chat in chats { + if let peer = transaction.getPeer(chat.peerId) { + peers.append(peer) + + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { + transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in + var current = current as? CachedChannelData ?? CachedChannelData() + var participantsSummary = current.participantsSummary + + participantsSummary.memberCount = participantsCount + + current = current.withUpdatedParticipantsSummary(participantsSummary) + return current + }) + } + } + } + + if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id), timestamp: CFAbsoluteTimeGetCurrent())) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.channelsForPublicReaction, key: ValueBoxKey(length: 0)), entry: entry) + } + + return peers + } + } + + if useLocalCache { + if let cachedPeers { + return .single(cachedPeers.0) + } else { + return .single([]) + } + } + + if let cachedPeers { + if CFAbsoluteTimeGetCurrent() < cachedPeers.1 + 5 * 60 { + return .single(cachedPeers.0) + } else { + return .single(cachedPeers.0) |> then(remote) + } + } else { + return remote + } + } +} + public enum ChannelAddressNameAssignmentAvailability { case available case unknown diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 873bddf159..c651e178c0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -144,6 +144,13 @@ public extension TelegramEngine { return peers.map(EnginePeer.init) } } + + public func channelsForPublicReaction(useLocalCache: Bool) -> Signal<[EnginePeer], NoError> { + return _internal_channelsForPublicReaction(account: self.account, useLocalCache: useLocalCache) + |> map { peers -> [EnginePeer] in + return peers.map(EnginePeer.init) + } + } public func channelAddressNameAssignmentAvailability(peerId: PeerId?) -> Signal { return _internal_channelAddressNameAssignmentAvailability(account: self.account, peerId: peerId) diff --git a/submodules/TelegramUI/Components/AvatarUploadToastScreen/Sources/AvatarUploadToastScreen.swift b/submodules/TelegramUI/Components/AvatarUploadToastScreen/Sources/AvatarUploadToastScreen.swift index 6adb8a69c4..00c1588cc6 100644 --- a/submodules/TelegramUI/Components/AvatarUploadToastScreen/Sources/AvatarUploadToastScreen.swift +++ b/submodules/TelegramUI/Components/AvatarUploadToastScreen/Sources/AvatarUploadToastScreen.swift @@ -218,19 +218,33 @@ private final class AvatarUploadToastScreenComponent: Component { let iconSize = CGSize(width: 30.0, height: 30.0) let iconProgressInset: CGFloat = 3.0 + let uploadingString = environment.strings.AvatarUpload_StatusUploading + let doneString = environment.strings.AvatarUpload_StatusDone + + var commonPrefixLength = 0 + for i in 0 ..< min(uploadingString.count, doneString.count) { + if uploadingString[uploadingString.index(uploadingString.startIndex, offsetBy: i)] != doneString[doneString.index(doneString.startIndex, offsetBy: i)] { + break + } + commonPrefixLength = i + } + var textItems: [AnimatedTextComponent.Item] = [] - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: true, content: .text("Your photo is "))) + + if commonPrefixLength != 0 { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: true, content: .text(String(uploadingString[uploadingString.startIndex ..< uploadingString.index(uploadingString.startIndex, offsetBy: commonPrefixLength)])))) + } if isDone { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text("now set."))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text(String(doneString[doneString.index(doneString.startIndex, offsetBy: commonPrefixLength)...])))) } else { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text("uploading."))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text(String(uploadingString[uploadingString.index(uploadingString.startIndex, offsetBy: commonPrefixLength)...])))) } let actionButtonSize = self.actionButton.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( content: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "View", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0))) + text: .plain(NSAttributedString(string: environment.strings.AvatarUpload_ViewAction, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0))) )), effectAlignment: .center, contentInsets: UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0), diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 4c731b68f8..bb8cb19604 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -86,7 +86,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: ComponentTransition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], isAnimatedOutToGroupCall: Bool, transition: ComponentTransition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -95,7 +95,9 @@ final class ButtonGroupView: OverlayMaskContainerView { let buttonNoticeSpacing: CGFloat = 16.0 let controlsHiddenNoticeSpacing: CGFloat = 0.0 var nextNoticeY: CGFloat - if controlsHidden { + if isAnimatedOutToGroupCall { + nextNoticeY = size.height + 4.0 + } else if controlsHidden { nextNoticeY = size.height - insets.bottom - 4.0 } else { nextNoticeY = size.height - insets.bottom - 52.0 - buttonSize - buttonNoticeSpacing @@ -130,9 +132,11 @@ final class ButtonGroupView: OverlayMaskContainerView { } } let noticeSize = noticeView.update(icon: notice.icon, text: notice.text, constrainedWidth: size.width - insets.left * 2.0 - 16.0 * 2.0, transition: noticeTransition) - let noticeFrame = CGRect(origin: CGPoint(x: floor((size.width - noticeSize.width) * 0.5), y: nextNoticeY - noticeSize.height), size: noticeSize) + let noticeFrame = CGRect(origin: CGPoint(x: floor((size.width - noticeSize.width) * 0.5), y: isAnimatedOutToGroupCall ? nextNoticeY : (nextNoticeY - noticeSize.height)), size: noticeSize) noticesHeight += noticeSize.height - nextNoticeY -= noticeSize.height + noticeSpacing + if !isAnimatedOutToGroupCall { + nextNoticeY -= noticeSize.height + noticeSpacing + } noticeTransition.setFrame(view: noticeView, frame: noticeFrame) if animateIn, !transition.animation.isImmediate { @@ -142,6 +146,9 @@ final class ButtonGroupView: OverlayMaskContainerView { if noticesHeight != 0.0 { noticesHeight += 5.0 } + if isAnimatedOutToGroupCall { + noticesHeight = 0.0 + } var removedNoticeIds: [AnyHashable] = [] for (id, noticeView) in self.noticeViews { if !validNoticeIds.contains(id) { @@ -161,7 +168,7 @@ final class ButtonGroupView: OverlayMaskContainerView { let buttonY: CGFloat let resultHeight: CGFloat - if controlsHidden { + if controlsHidden || isAnimatedOutToGroupCall { buttonY = size.height + 12.0 resultHeight = insets.bottom + 4.0 + noticesHeight } else { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ConferenceButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ConferenceButtonView.swift new file mode 100644 index 0000000000..c04926106a --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ConferenceButtonView.swift @@ -0,0 +1,104 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import UIKitRuntimeUtils +import AppBundle + +final class ConferenceButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol { + private struct Params: Equatable { + var size: CGSize + + init(size: CGSize) { + self.size = size + } + } + + private let backdropBackgroundView: RoundedCornersView + private let iconView: UIImageView + + var pressAction: (() -> Void)? + + private var params: Params? + + let maskContents: UIView + override static var layerClass: AnyClass { + return MirroringLayer.self + } + + override init(frame: CGRect) { + self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true) + + self.iconView = UIImageView() + + self.maskContents = UIView() + self.maskContents.addSubview(self.backdropBackgroundView) + + super.init(frame: frame) + + self.addSubview(self.iconView) + + (self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer + + self.internalHighligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } else { + let t = self.layer.presentation()?.transform ?? layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + let transition = ComponentTransition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in + guard let self, completed else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.pressAction?() + } + + func update(size: CGSize, transition: ComponentTransition) { + let params = Params(size: size) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: ComponentTransition) { + self.backdropBackgroundView.update(cornerRadius: params.size.height * 0.5, transition: transition) + transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + + if self.iconView.image == nil { + self.iconView.image = UIImage(bundleImageName: "Contact List/AddMemberIcon")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = .white + } + + if let image = self.iconView.image { + let fraction: CGFloat = 1.0 + let imageSize = CGSize(width: floor(image.size.width * fraction), height: floor(image.size.height * fraction)) + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((params.size.width - imageSize.width) * 0.5), y: floorToScreenPixels((params.size.height - imageSize.height) * 0.5)), size: imageSize)) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift index 68addda2d2..4eb3db1ff5 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -29,7 +29,7 @@ public func resolveCallVideoRotationAngle(angle: Float, followsDeviceOrientation return (angle + interfaceAngle).truncatingRemainder(dividingBy: Float.pi * 2.0) } -private final class VideoContainerLayer: SimpleLayer { +final class VideoContainerLayer: SimpleLayer { let contentsLayer: SimpleLayer override init() { @@ -129,11 +129,16 @@ final class VideoContainerView: HighlightTrackingButton { let key: Key - private let videoContainerLayer: VideoContainerLayer + let videoContainerLayer: VideoContainerLayer + var videoContainerLayerTaken: Bool = false private var videoLayer: PrivateCallVideoLayer private var disappearingVideoLayer: DisappearingVideo? + var currentVideoOutput: VideoSource.Output? { + return self.videoLayer.video + } + let blurredContainerLayer: SimpleLayer private let shadowContainer: SimpleLayer @@ -245,7 +250,7 @@ final class VideoContainerView: HighlightTrackingButton { self.layer.addSublayer(self.shadowContainer) self.highligthedChanged = { [weak self] highlighted in - guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty else { + guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty, !self.videoContainerLayerTaken else { return } var highlightedState = false @@ -316,6 +321,10 @@ final class VideoContainerView: HighlightTrackingButton { } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + if self.videoContainerLayerTaken { + return + } + switch recognizer.state { case .began, .changed: self.dragVelocity = CGPoint() @@ -549,6 +558,9 @@ final class VideoContainerView: HighlightTrackingButton { } private func update(previousParams: Params?, params: Params, transition: ComponentTransition) { + if self.videoContainerLayerTaken { + return + } guard let videoMetrics = self.videoMetrics else { return } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 8073195d30..edb759caee 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -80,6 +80,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool public var isEnergySavingEnabled: Bool + public var isConferencePossible: Bool public init( strings: PresentationStrings, @@ -93,7 +94,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu localVideo: VideoSource?, remoteVideo: VideoSource?, isRemoteBatteryLow: Bool, - isEnergySavingEnabled: Bool + isEnergySavingEnabled: Bool, + isConferencePossible: Bool ) { self.strings = strings self.lifecycleState = lifecycleState @@ -107,6 +109,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow self.isEnergySavingEnabled = isEnergySavingEnabled + self.isConferencePossible = isConferencePossible } public static func ==(lhs: State, rhs: State) -> Bool { @@ -146,6 +149,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if lhs.isEnergySavingEnabled != rhs.isEnergySavingEnabled { return false } + if lhs.isConferencePossible != rhs.isConferencePossible { + return false + } return true } } @@ -178,6 +184,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private let avatarLayer: AvatarLayer private let titleView: TextView private let backButtonView: BackButtonView + private var conferenceButtonView: ConferenceButtonView? private var statusView: StatusView private var weakSignalView: WeakSignalView? @@ -199,6 +206,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private var isUpdating: Bool = false + private var isAnimatedOutToGroupCall: Bool = false + private var animateOutToGroupCallCompletion: (() -> Void)? + private var canAnimateAudioLevel: Bool = false private var displayEmojiTooltip: Bool = false private var isEmojiKeyExpanded: Bool = false @@ -233,8 +243,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private var pipVideoCallViewController: UIViewController? private var pipController: AVPictureInPictureController? - private var snowEffectView: SnowEffectView? - public override init(frame: CGRect) { self.overlayContentsView = UIView() self.overlayContentsView.isUserInteractionEnabled = false @@ -489,6 +497,32 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } + public func animateOutToGroupChat(completion: @escaping () -> Void) { + self.isAnimatedOutToGroupCall = true + self.animateOutToGroupCallCompletion = completion + self.update(transition: .easeInOut(duration: 0.25)) + } + + public func takeIncomingVideoLayer() -> (CALayer, VideoSource.Output?)? { + var remoteVideoContainerKey: VideoContainerView.Key? + if self.swapLocalAndRemoteVideo { + if let _ = self.activeRemoteVideoSource { + remoteVideoContainerKey = .foreground + } + } else { + if let _ = self.activeRemoteVideoSource { + remoteVideoContainerKey = .background + } + } + + if let remoteVideoContainerKey, let videoContainerView = self.videoContainerViews.first(where: { $0.key == remoteVideoContainerKey }) { + videoContainerView.videoContainerLayerTaken = true + return (videoContainerView.videoContainerLayer, videoContainerView.currentVideoOutput) + } + + return nil + } + public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: ComponentTransition) { let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state) if self.params == params { @@ -717,6 +751,16 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } self.backgroundLayer.update(stateIndex: backgroundStateIndex, isEnergySavingEnabled: params.state.isEnergySavingEnabled, transition: transition) + genericAlphaTransition.setAlpha(layer: self.backgroundLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0, completion: { [weak self] _ in + guard let self else { + return + } + if let animateOutToGroupCallCompletion = self.animateOutToGroupCallCompletion { + self.animateOutToGroupCallCompletion = nil + animateOutToGroupCallCompletion() + } + }) + transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size)) var isVideoButtonEnabled = false @@ -793,7 +837,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu }*/ let displayClose = false - let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, transition: transition) + let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, isAnimatedOutToGroupCall: self.isAnimatedOutToGroupCall, transition: transition) var expandedEmojiKeyRect: CGRect? if self.isEmojiKeyExpanded { @@ -836,7 +880,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height)) emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size)) - alphaTransition.setAlpha(view: emojiExpandedInfoView, alpha: 1.0) + alphaTransition.setAlpha(view: emojiExpandedInfoView, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) transition.setScale(view: emojiExpandedInfoView, scale: 1.0) expandedEmojiKeyRect = emojiExpandedInfoFrame @@ -868,9 +912,55 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: backButtonSize) transition.setFrame(view: self.backButtonView, frame: backButtonFrame) - transition.setAlpha(view: self.backButtonView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(view: self.backButtonView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) - if case let .active(activeState) = params.state.lifecycleState { + + var isConferencePossible = false + if case .active = params.state.lifecycleState, params.state.isConferencePossible { + isConferencePossible = true + } + + if isConferencePossible { + let conferenceButtonView: ConferenceButtonView + var conferenceButtonTransition = transition + if let current = self.conferenceButtonView { + conferenceButtonView = current + } else { + conferenceButtonTransition = conferenceButtonTransition.withAnimation(.none) + conferenceButtonView = ConferenceButtonView() + conferenceButtonView.alpha = 0.0 + self.conferenceButtonView = conferenceButtonView + self.addSubview(conferenceButtonView) + + conferenceButtonView.pressAction = { [weak self] in + guard let self else { + return + } + self.conferenceAddParticipant?() + } + } + + let conferenceButtonSize = CGSize(width: 40.0, height: 40.0) + conferenceButtonView.update(size: conferenceButtonSize, transition: conferenceButtonTransition) + + let conferenceButtonY: CGFloat + if currentAreControlsHidden { + conferenceButtonY = -conferenceButtonSize.height - 3.0 + } else { + conferenceButtonY = params.insets.top + 3.0 + } + let conferenceButtonFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 10.0 - conferenceButtonSize.width, y: conferenceButtonY), size: conferenceButtonSize) + + conferenceButtonTransition.setFrame(view: conferenceButtonView, frame: conferenceButtonFrame) + genericAlphaTransition.setAlpha(view: conferenceButtonView, alpha: 1.0) + } else { + if let conferenceButtonView = self.conferenceButtonView { + self.conferenceButtonView = nil + conferenceButtonView.removeFromSuperview() + } + } + + if !isConferencePossible, case let .active(activeState) = params.state.lifecycleState { let emojiView: KeyEmojiView var emojiTransition = transition var emojiAlphaTransition = genericAlphaTransition @@ -915,6 +1005,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center) } emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size)) + if self.isAnimatedOutToGroupCall { + emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) + } if let emojiTooltipView = self.emojiTooltipView { self.emojiTooltipView = nil @@ -940,7 +1033,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center) } emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size)) - emojiAlphaTransition.setAlpha(view: emojiView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) if self.displayEmojiTooltip { let emojiTooltipView: EmojiTooltipView @@ -1261,6 +1354,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } + genericAlphaTransition.setAlpha(layer: self.avatarTransformLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(layer: self.blobTransformLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) + let titleSize = self.titleView.update( string: titleString, fontSize: !havePrimaryVideo ? 28.0 : 17.0, @@ -1335,7 +1431,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu size: titleSize ) transition.setFrame(view: self.titleView, frame: titleFrame) - genericAlphaTransition.setAlpha(view: self.titleView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(view: self.titleView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) let statusFrame = CGRect( origin: CGPoint( @@ -1354,7 +1450,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } else { transition.setFrame(view: self.statusView, frame: statusFrame) - genericAlphaTransition.setAlpha(view: self.statusView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(view: self.statusView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) } if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2, !self.isEmojiKeyExpanded, (!self.displayEmojiTooltip || !havePrimaryVideo) { @@ -1380,11 +1476,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu ComponentTransition.immediate.setScale(view: weakSignalView, scale: 0.001) weakSignalView.alpha = 0.0 transition.setScaleWithSpring(view: weakSignalView, scale: 1.0) - transition.setAlpha(view: weakSignalView, alpha: 1.0) } } else { transition.setFrame(view: weakSignalView, frame: weakSignalFrame) } + transition.setAlpha(view: weakSignalView, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) } else { if let weakSignalView = self.weakSignalView { self.weakSignalView = nil @@ -1396,55 +1492,3 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } } - -final class SnowEffectView: UIView { - private let particlesLayer: CAEmitterLayer - - override init(frame: CGRect) { - let particlesLayer = CAEmitterLayer() - self.particlesLayer = particlesLayer - self.particlesLayer.backgroundColor = nil - self.particlesLayer.isOpaque = false - - particlesLayer.emitterShape = .circle - particlesLayer.emitterMode = .surface - particlesLayer.renderMode = .oldestLast - - let image1 = UIImage(named: "Call/Snow")?.cgImage - - let cell1 = CAEmitterCell() - cell1.contents = image1 - cell1.name = "Snow" - cell1.birthRate = 92.0 - cell1.lifetime = 20.0 - cell1.velocity = 59.0 - cell1.velocityRange = -15.0 - cell1.xAcceleration = 5.0 - cell1.yAcceleration = 40.0 - cell1.emissionRange = 90.0 * (.pi / 180.0) - cell1.spin = -28.6 * (.pi / 180.0) - cell1.spinRange = 57.2 * (.pi / 180.0) - cell1.scale = 0.06 - cell1.scaleRange = 0.3 - cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor - - particlesLayer.emitterCells = [cell1] - - super.init(frame: frame) - - self.layer.addSublayer(particlesLayer) - self.clipsToBounds = true - self.backgroundColor = nil - self.isOpaque = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(size: CGSize) { - self.particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) - self.particlesLayer.emitterSize = CGSize(width: size.width * 3.0, height: size.height * 2.0) - self.particlesLayer.emitterPosition = CGPoint(x: size.width * 0.5, y: -325.0) - } -} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD index 660c84d84c..31db545b01 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/Utils/RangeSet", + "//submodules/MediaResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index f7ccee5ae3..7bea53e5cf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -33,6 +33,7 @@ import WallpaperPreviewMedia import TextNodeWithEntities import RangeSet import GiftItemComponent +import MediaResources private struct FetchControls { let fetch: (Bool) -> Void @@ -444,6 +445,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public let dateAndStatusNode: ChatMessageDateAndStatusNode private var badgeNode: ChatMessageInteractiveMediaBadge? + private var timestampContainerView: UIView? + private var timestampMaskView: UIImageView? + private var videoTimestampBackgroundLayer: SimpleLayer? + private var videoTimestampForegroundLayer: SimpleLayer? + private var extendedMediaOverlayNode: ExtendedMediaOverlayNode? private var context: AccountContext? @@ -620,6 +626,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr transition.updateAlpha(node: statusNode, alpha: 1.0 - factor) } } + + self.imageNode.imageUpdated = { [weak self] image in + guard let self else { + return + } + self.timestampMaskView?.image = image + } } deinit { @@ -1937,6 +1950,96 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } + //TODO:wip-release + /*var videoTimestamp: Int32? + var storedVideoTimestamp: Int32? + for attribute in message.attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp + } else if let attribute = attribute as? DerivedDataMessageAttribute { + if let value = attribute.data["mps"]?.get(MediaPlaybackStoredState.self) { + storedVideoTimestamp = Int32(value.timestamp) + } + } + } + if let storedVideoTimestamp { + videoTimestamp = storedVideoTimestamp + } + + if let videoTimestamp, let file = media as? TelegramMediaFile, let duration = file.duration, duration > 1.0 { + let timestampContainerView: UIView + if let current = strongSelf.timestampContainerView { + timestampContainerView = current + } else { + timestampContainerView = UIView() + timestampContainerView.isUserInteractionEnabled = false + strongSelf.timestampContainerView = timestampContainerView + strongSelf.view.addSubview(timestampContainerView) + } + + let timestampMaskView: UIImageView + if let current = strongSelf.timestampMaskView { + timestampMaskView = current + } else { + timestampMaskView = UIImageView() + strongSelf.timestampMaskView = timestampMaskView + timestampContainerView.mask = timestampMaskView + + timestampMaskView.image = strongSelf.imageNode.image + } + + let videoTimestampBackgroundLayer: SimpleLayer + if let current = strongSelf.videoTimestampBackgroundLayer { + videoTimestampBackgroundLayer = current + } else { + videoTimestampBackgroundLayer = SimpleLayer() + strongSelf.videoTimestampBackgroundLayer = videoTimestampBackgroundLayer + timestampContainerView.layer.addSublayer(videoTimestampBackgroundLayer) + } + + let videoTimestampForegroundLayer: SimpleLayer + if let current = strongSelf.videoTimestampForegroundLayer { + videoTimestampForegroundLayer = current + } else { + videoTimestampForegroundLayer = SimpleLayer() + strongSelf.videoTimestampForegroundLayer = videoTimestampForegroundLayer + timestampContainerView.layer.addSublayer(videoTimestampForegroundLayer) + } + + videoTimestampBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor + videoTimestampForegroundLayer.backgroundColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.accentControlColor.cgColor : presentationData.theme.theme.chat.message.outgoing.accentControlColor.cgColor + + timestampContainerView.frame = imageFrame + timestampMaskView.frame = imageFrame + + let videoTimestampBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: imageFrame.height - 3.0), size: CGSize(width: imageFrame.width, height: 3.0)) + videoTimestampBackgroundLayer.frame = videoTimestampBackgroundFrame + + var fraction = Double(videoTimestamp) / duration + fraction = max(0.0, min(1.0, fraction)) + + let foregroundWidth = round(fraction * videoTimestampBackgroundFrame.width) + let videoTimestampForegroundFrame = CGRect(origin: CGPoint(x: videoTimestampBackgroundFrame.minX, y: videoTimestampBackgroundFrame.minY), size: CGSize(width: foregroundWidth, height: videoTimestampBackgroundFrame.height)) + videoTimestampForegroundLayer.frame = videoTimestampForegroundFrame + } else { + if let timestampContainerView = strongSelf.timestampContainerView { + strongSelf.timestampContainerView = nil + timestampContainerView.removeFromSuperview() + } + if let timestampMaskView = strongSelf.timestampMaskView { + strongSelf.timestampMaskView = nil + timestampMaskView.removeFromSuperview() + } + if let videoTimestampBackgroundLayer = strongSelf.videoTimestampBackgroundLayer { + strongSelf.videoTimestampBackgroundLayer = nil + videoTimestampBackgroundLayer.removeFromSuperlayer() + } + if let videoTimestampForegroundLayer = strongSelf.videoTimestampForegroundLayer { + strongSelf.videoTimestampForegroundLayer = nil + videoTimestampForegroundLayer.removeFromSuperlayer() + } + }*/ + if let animatedStickerNode = strongSelf.animatedStickerNode { animatedStickerNode.frame = imageFrame animatedStickerNode.updateLayout(size: imageFrame.size) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index aa3ec3194d..a7d9564ab0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -21,6 +21,7 @@ import BundleIconComponent import CheckNode import TextFormat import CheckComponent +import ContextUI private final class BalanceComponent: CombinedComponent { let context: AccountContext @@ -821,6 +822,8 @@ private final class ChatSendStarsScreenComponent: Component { let context: AccountContext let peer: EnginePeer let myPeer: EnginePeer + let sendAsPeer: EnginePeer + let channelsForPublicReaction: [EnginePeer] let messageId: EngineMessage.Id let maxAmount: Int let balance: StarsAmount? @@ -833,6 +836,8 @@ private final class ChatSendStarsScreenComponent: Component { context: AccountContext, peer: EnginePeer, myPeer: EnginePeer, + sendAsPeer: EnginePeer, + channelsForPublicReaction: [EnginePeer], messageId: EngineMessage.Id, maxAmount: Int, balance: StarsAmount?, @@ -844,6 +849,8 @@ private final class ChatSendStarsScreenComponent: Component { self.context = context self.peer = peer self.myPeer = myPeer + self.sendAsPeer = sendAsPeer + self.channelsForPublicReaction = channelsForPublicReaction self.messageId = messageId self.maxAmount = maxAmount self.balance = balance @@ -863,6 +870,12 @@ private final class ChatSendStarsScreenComponent: Component { if lhs.myPeer != rhs.myPeer { return false } + if lhs.sendAsPeer != rhs.sendAsPeer { + return false + } + if lhs.channelsForPublicReaction != rhs.channelsForPublicReaction { + return false + } if lhs.maxAmount != rhs.maxAmount { return false } @@ -988,6 +1001,7 @@ private final class ChatSendStarsScreenComponent: Component { private let hierarchyTrackingNode: HierarchyTrackingNode private let leftButton = ComponentView() + private let peerSelectorButton = ComponentView() private let closeButton = ComponentView() private let title = ComponentView() @@ -1037,6 +1051,10 @@ private final class ChatSendStarsScreenComponent: Component { private var badgePhysicsLink: SharedDisplayLinkDriver.Link? + private var currentMyPeer: EnginePeer? + private var channelsForPublicReaction: [EnginePeer] = [] + private var channelsForPublicReactionDisposable: Disposable? + override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -1119,6 +1137,7 @@ private final class ChatSendStarsScreenComponent: Component { deinit { self.balanceDisposable?.dispose() + self.channelsForPublicReactionDisposable?.dispose() } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -1297,6 +1316,87 @@ private final class ChatSendStarsScreenComponent: Component { } } + private func displayTargetSelectionMenu(sourceView: UIView) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + + var items: [ContextMenuItem] = [] + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + var peers: [EnginePeer] = [component.myPeer] + peers.append(contentsOf: self.channelsForPublicReaction) + + let avatarSize = CGSize(width: 30.0, height: 30.0) + + for peer in peers { + let peerLabel: String + if peer.id == component.context.account.peerId { + peerLabel = environment.strings.AffiliateProgram_PeerTypeSelf + } else if case .channel = peer { + peerLabel = environment.strings.Channel_Status + } else { + peerLabel = environment.strings.Bot_GenericBotStatus + } + let isSelected = peer.id == self.currentMyPeer?.id + let accentColor = environment.theme.list.itemAccentColor + let avatarSignal = peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize) + |> map { image in + let context = DrawingContext(size: avatarSize, scale: 0.0, clear: true) + context?.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + if isSelected { + + } + c.saveGState() + let scaleFactor = (avatarSize.width - 3.0 * 2.0) / avatarSize.width + if isSelected { + c.translateBy(x: avatarSize.width * 0.5, y: avatarSize.height * 0.5) + c.scaleBy(x: scaleFactor, y: scaleFactor) + c.translateBy(x: -avatarSize.width * 0.5, y: -avatarSize.height * 0.5) + } + if let image { + image.draw(in: CGRect(origin: CGPoint(), size: avatarSize)) + } + c.restoreGState() + + if isSelected { + c.setStrokeColor(accentColor.cgColor) + let lineWidth: CGFloat = 1.0 + UIScreenPixel + c.setLineWidth(lineWidth) + c.strokeEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + } + } + return context?.generateImage() + } + items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { [weak self] c, _ in + c?.dismiss(completion: {}) + + guard let self, let component = self.component else { + return + } + if self.currentMyPeer?.id == peer.id { + return + } + + if self.currentMyPeer != peer { + self.currentMyPeer = peer + + let _ = component.context.engine.peers.updatePeerSendAsPeer(peerId: component.peer.id, sendAs: peer.id).startStandalone() + } + + self.state?.updated(transition: .immediate) + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -1312,6 +1412,8 @@ private final class ChatSendStarsScreenComponent: Component { let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 16.0 if self.component == nil { + self.currentMyPeer = component.myPeer + self.balance = component.balance var isLogarithmic = true if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_stars_reaction_logarithmic_scale"] as? Double { @@ -1336,6 +1438,17 @@ private final class ChatSendStarsScreenComponent: Component { } }) } + + self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false) + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + guard let self else { + return + } + if self.channelsForPublicReaction != peers { + self.channelsForPublicReaction = peers + self.state?.updated(transition: .immediate) + } + }) } self.component = component @@ -1514,6 +1627,9 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 123.0 + var sendAsPeers: [EnginePeer] = [component.myPeer] + sendAsPeers.append(contentsOf: self.channelsForPublicReaction) + let leftButtonSize = self.leftButton.update( transition: transition, component: AnyComponent(BalanceComponent( @@ -1531,6 +1647,35 @@ private final class ChatSendStarsScreenComponent: Component { self.navigationBarContainer.addSubview(leftButtonView) } transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + leftButtonView.isHidden = sendAsPeers.count > 1 + } + + let currentMyPeer = self.currentMyPeer ?? component.myPeer + + let peerSelectorButtonSize = self.peerSelectorButton.update( + transition: transition, + component: AnyComponent(PeerSelectorBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: currentMyPeer, + action: { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((56.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) + if let peerSelectorButtonView = self.peerSelectorButton.view { + if peerSelectorButtonView.superview == nil { + self.navigationBarContainer.addSubview(peerSelectorButtonView) + } + transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) + peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } if themeUpdated { @@ -1717,7 +1862,7 @@ private final class ChatSendStarsScreenComponent: Component { if myCount != 0 { mappedTopPeers.append(ChatSendStarsScreen.TopPeer( randomIndex: -1, - peer: self.isAnonymous ? nil : component.myPeer, + peer: self.isAnonymous ? nil : currentMyPeer, isMy: true, count: myCount )) @@ -2102,6 +2247,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { public final class InitialData { fileprivate let peer: EnginePeer fileprivate let myPeer: EnginePeer + fileprivate let sendAsPeer: EnginePeer + fileprivate let channelsForPublicReaction: [EnginePeer] fileprivate let messageId: EngineMessage.Id fileprivate let balance: StarsAmount? fileprivate let currentSentAmount: Int? @@ -2111,6 +2258,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { fileprivate init( peer: EnginePeer, myPeer: EnginePeer, + sendAsPeer: EnginePeer, + channelsForPublicReaction: [EnginePeer], messageId: EngineMessage.Id, balance: StarsAmount?, currentSentAmount: Int?, @@ -2119,6 +2268,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { ) { self.peer = peer self.myPeer = myPeer + self.sendAsPeer = sendAsPeer + self.channelsForPublicReaction = channelsForPublicReaction self.messageId = messageId self.balance = balance self.currentSentAmount = currentSentAmount @@ -2204,6 +2355,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { context: context, peer: initialData.peer, myPeer: initialData.myPeer, + sendAsPeer: initialData.sendAsPeer, + channelsForPublicReaction: initialData.channelsForPublicReaction, messageId: initialData.messageId, maxAmount: maxAmount, balance: initialData.balance, @@ -2266,15 +2419,22 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { topPeers = Array(topPeers.prefix(3)) } + //TODO:wip-release + //let channelsForPublicReaction = context.engine.peers.channelsForPublicReaction(useLocalCache: true) + let channelsForPublicReaction: Signal<[EnginePeer], NoError> = .single([]) + let sendAsPeer: Signal = .single(nil) + return combineLatest( context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), EngineDataMap(allPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ), - balance + balance, + sendAsPeer, + channelsForPublicReaction ) - |> map { peerAndTopPeerMap, balance -> InitialData? in + |> map { peerAndTopPeerMap, balance, sendAsPeer, channelsForPublicReaction -> InitialData? in let (peer, myPeer, topPeerMap) = peerAndTopPeerMap guard let peer, let myPeer else { return nil @@ -2284,6 +2444,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { return InitialData( peer: peer, myPeer: myPeer, + sendAsPeer: sendAsPeer ?? myPeer, + channelsForPublicReaction: channelsForPublicReaction, messageId: messageId, balance: balance, currentSentAmount: currentSentAmount, @@ -2570,3 +2732,172 @@ private final class SliderStarsView: UIView { self.emitterLayer.emitterSize = size } } + +private final class PeerSelectorBadgeComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + let action: ((UIView) -> Void)? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer, + action: ((UIView) -> Void)? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.action = action + } + + static func ==(lhs: PeerSelectorBadgeComponent, rhs: PeerSelectorBadgeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: HighlightableButton { + private let background = ComponentView() + private var avatarNode: AvatarNode? + private var selectorIcon: ComponentView? + + private var component: PeerSelectorBadgeComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action?(self) + } + + func update(component: PeerSelectorBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.isEnabled = component.action != nil + + let height: CGFloat = 32.0 + let avatarPadding: CGFloat = 1.0 + + let avatarDiameter = height - avatarPadding * 2.0 + let avatarTextSpacing: CGFloat = -4.0 + let rightTextInset: CGFloat = component.action != nil ? 26.0 : 12.0 + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(avatarDiameter * 0.5))) + avatarNode.isUserInteractionEnabled = false + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + avatarNode.frame = avatarFrame + avatarNode.updateSize(size: avatarFrame.size) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, synchronousLoad: true) + + let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + rightTextInset, height: height) + + if component.action != nil { + let selectorIcon: ComponentView + if let current = self.selectorIcon { + selectorIcon = current + } else { + selectorIcon = ComponentView() + self.selectorIcon = selectorIcon + } + let selectorIconSize = selectorIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Item List/ExpandableSelectorArrows", tintColor: component.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5))), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize) + if let selectorIconView = selectorIcon.view { + if selectorIconView.superview == nil { + selectorIconView.isUserInteractionEnabled = false + self.addSubview(selectorIconView) + } + transition.setFrame(view: selectorIconView, frame: selectorIconFrame) + } + } else if let selectorIcon = self.selectorIcon { + self.selectorIcon = nil + selectorIcon.view?.removeFromSuperview() + } + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.theme.list.itemInputField.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + private let actionsOnTop: Bool + + init(controller: ViewController, sourceView: UIView, actionsOnTop: Bool) { + self.controller = controller + self.sourceView = sourceView + self.actionsOnTop = actionsOnTop + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 3df171977a..a9ed9eff37 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -100,6 +100,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { private let title = ComponentView() private let subtitle = ComponentView() + private let openBotButton = ComponentView() private var dailyRevenueText: ComponentView? private let titleTransformContainer: UIView private let bottomPanelContainer: UIView @@ -832,6 +833,83 @@ private final class JoinAffiliateProgramScreenComponent: Component { self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + var openBotComponents: [AnyComponentWithIdentity] = [] + var openBotLeftInset: CGFloat = 12.0 + if case .active = component.mode { + openBotLeftInset = 1.0 + openBotComponents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(AvatarComponent( + context: component.context, + peer: component.sourcePeer, + size: CGSize(width: 30.0, height: 30.0) + )), fixedSize: CGSize(width: 30.0, height: 2.0), + translation: CGPoint(x: 0.0, y: 1.0))))) + } + openBotComponents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.AffiliateProgram_OpenBot(component.sourcePeer.compactDisplayTitle).string, font: Font.medium(15.0), textColor: environment.theme.list.itemInputField.primaryColor)) + )))) + openBotComponents.append(AnyComponentWithIdentity(id: 2, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: environment.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5), + scaleFactor: 0.8 + )), + fixedSize: CGSize(width: 8.0, height: 2.0), + translation: CGPoint(x: 0.0, y: 2.0) + )))) + let openBotButtonSize = self.openBotButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack(openBotComponents, spacing: 2.0)), + background: AnyComponent(FilledRoundedRectangleComponent(color: environment.theme.list.itemInputField.backgroundColor, cornerRadius: .minEdge, smoothCorners: false)), + effectAlignment: .center, + minSize: CGSize(width: 1.0, height: 30.0 + 2.0), + contentInsets: UIEdgeInsets(top: 0.0, left: openBotLeftInset, bottom: 0.0, right: 12.0), + action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + guard let infoController = component.context.sharedContext.makePeerInfoController( + context: component.context, + updatedPresentationData: nil, + peer: component.sourcePeer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) else { + return + } + controller.dismiss(completion: { [weak navigationController] in + DispatchQueue.main.async { + guard let navigationController else { + return + } + navigationController.pushViewController(infoController) + } + }) + }, + animateAlpha: true, + animateScale: true, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let openBotButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - openBotButtonSize.width) * 0.5), y: contentHeight), size: openBotButtonSize) + if let openBotButtonView = self.openBotButton.view { + if openBotButtonView.superview == nil { + self.scrollContentView.addSubview(openBotButtonView) + } + transition.setPosition(view: openBotButtonView, position: openBotButtonFrame.center) + openBotButtonView.bounds = CGRect(origin: CGPoint(), size: openBotButtonFrame.size) + } + contentHeight += openBotButtonSize.height + contentHeight += 20.0 + let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 0336afd47c..1dad042ac5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -6266,7 +6266,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil) - let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact)), updatedPresentationData: strongSelf.controller?.updatedPresentationData) + let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact), nil), updatedPresentationData: strongSelf.controller?.updatedPresentationData) shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 0273a7f3ac..d7c31a35d2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2599,7 +2599,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let shareController = ShareController( context: self.context, - subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false))), + subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false)), nil), presetText: nil, preferredAction: .default, showInChat: nil, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 5bcd1fc15b..5bacb94618 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1035,7 +1035,7 @@ final class StoryItemSetContainerSendMessage { let shareController = ShareController( context: component.context, - subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false))), + subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false)), nil), preferredAction: preferredAction ?? .default, externalShare: false, immediateExternalShare: false, diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/Contents.json new file mode 100644 index 0000000000..acc6641f90 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrows.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/arrows.pdf b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/arrows.pdf new file mode 100644 index 0000000000..31e7762d53 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/arrows.pdf @@ -0,0 +1,88 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.834991 2.770020 cm +0.000000 0.000000 0.000000 scn +4.135217 12.200247 m +4.010505 12.324958 3.841359 12.395020 3.664990 12.395020 c +3.488621 12.395020 3.319476 12.324956 3.194765 12.200244 c +0.194780 9.200245 l +-0.064918 8.940545 -0.064917 8.519490 0.194782 8.259792 c +0.454482 8.000094 0.875536 8.000095 1.135234 8.259794 c +3.664994 10.789568 l +6.194782 8.259792 l +6.454482 8.000094 6.875536 8.000095 7.135234 8.259794 c +7.394932 8.519494 7.394931 8.940549 7.135232 9.200247 c +4.135217 12.200247 l +h +7.135226 3.259784 m +4.135226 0.259784 l +3.875527 0.000086 3.454473 0.000086 3.194774 0.259784 c +0.194774 3.259784 l +-0.064925 3.519483 -0.064925 3.940537 0.194774 4.200236 c +0.454473 4.459935 0.875527 4.459935 1.135226 4.200236 c +3.665000 1.670462 l +6.194774 4.200236 l +6.454473 4.459935 6.875527 4.459935 7.135226 4.200236 c +7.394925 3.940537 7.394925 3.519483 7.135226 3.259784 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 959 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 9.000000 18.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001049 00000 n +0000001071 00000 n +0000001243 00000 n +0000001317 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1376 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index a598a5a4a1..c28cea930f 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -305,7 +305,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } override func loadDisplayNode() { - self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, attemptDisabledItemSelection: self.attemptDisabledItemSelection, options: self.options, filters: self.filters, onlyWriteable: self.onlyWriteable, isGroupInvitation: self.isGroupInvitation, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit, present: { [weak self] c, a in + self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, updatedPresentationData: self.params.updatedPresentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, attemptDisabledItemSelection: self.attemptDisabledItemSelection, options: self.options, filters: self.filters, onlyWriteable: self.onlyWriteable, isGroupInvitation: self.isGroupInvitation, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }) switch self.contactsNode.contentNode { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index b136def091..efb93aee9b 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -82,7 +82,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let onlyWriteable: Bool private let isGroupInvitation: Bool - init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { + init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { self.navigationBar = navigationBar self.context = context @@ -241,7 +241,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { return .natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers) } - let contactListNode = ContactListNode(context: context, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState()) + let contactListNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState()) self.contentNode = .contacts(contactListNode) if !selectedPeers.isEmpty { @@ -365,7 +365,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { case .premiumGifting, .requestedUsersSelection: searchChatList = true } - let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(ContactListPresentation.Search( + let searchResultsNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: .single(.search(ContactListPresentation.Search( signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, @@ -373,7 +373,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { searchChannels: searchChannels, globalSearch: globalSearch, displaySavedMessages: displaySavedMessages - ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) + ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) searchResultsNode.openPeer = { peer, _, _, _ in self?.tokenListNode.setText("") self?.openPeer?(peer) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 86aa4c4867..123073f5d6 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -227,7 +227,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } if immediateShare { - let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true) + let controller = ShareController(context: params.context, subject: .media(.standalone(media: file), nil), immediateExternalShare: true) params.present(controller, nil, .window(.root)) } else if let rootController = params.navigationController?.view.window?.rootViewController { let proceed = { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3100b5c984..053aebe5c8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -176,6 +176,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let callState = Promise(nil) private var awaitingCallConnectionDisposable: Disposable? private var callPeerDisposable: Disposable? + private var callIsConferenceDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -811,18 +812,41 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callController?.dismiss() self.callController = nil self.hasOngoingCall.set(false) + self.callState.set(.single(nil)) self.notificationController?.setBlocking(nil) self.callPeerDisposable?.dispose() self.callPeerDisposable = nil + self.callIsConferenceDisposable?.dispose() + self.callIsConferenceDisposable = nil if let call { - self.callState.set(call.state - |> map(Optional.init)) + if call.conferenceCall == nil { + self.callState.set(call.state + |> map(Optional.init)) + } + self.hasOngoingCall.set(true) setNotificationCall(call) + self.callIsConferenceDisposable = (call.hasConference + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + guard let call = self.call else { + return + } + guard let callController = self.callController, callController.call === call else { + return + } + if call.conferenceCall != nil { + self.callState.set(.single(nil)) + self.presentControllerWithCurrentCall() + } + }) + if call.isOutgoing { self.presentControllerWithCurrentCall() } else { @@ -921,7 +945,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return } - let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData) + let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData, sourceCallController: nil) groupCallController.onViewDidAppear = { [weak strongSelf] in if let strongSelf { strongSelf.hasGroupCallOnScreenPromise.set(true) @@ -947,31 +971,34 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) - let callSignal: Signal = .single(nil) + let callSignal: Signal<(PresentationCall?, PresentationGroupCall?), NoError> = .single((nil, nil)) |> then( callManager.currentCallSignal |> deliverOnMainQueue - |> mapToSignal { call -> Signal in + |> mapToSignal { call -> Signal<(PresentationCall?, PresentationGroupCall?), NoError> in guard let call else { - return .single(nil) + return .single((nil, nil)) } - return call.state - |> map { [weak call] state -> PresentationCall? in + return combineLatest(call.state, call.hasConference) + |> map { [weak call] state, _ -> (PresentationCall?, PresentationGroupCall?) in guard let call else { - return nil + return (nil, nil) + } + if let conferenceCall = call.conferenceCall { + return (nil, conferenceCall) } switch state.state { case .ringing: - return nil + return (nil, nil) case .terminating, .terminated: - return nil + return (nil, nil) default: - return call + return (call, nil) } } } |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs === rhs + return lhs.0 === rhs.0 && lhs.1 === rhs.1 }) ) let groupCallSignal: Signal = .single(nil) @@ -985,8 +1012,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.hasGroupCallOnScreenPromise.get() ).start(next: { [weak self] call, groupCall, hasGroupCallOnScreen in if let strongSelf = self { + var (call, conferenceCall) = call + var groupCall = groupCall + if let conferenceCall { + call = nil + groupCall = conferenceCall + } + let statusBarContent: CallStatusBarNodeImpl.Content? - if let call = call { + if let call, !hasGroupCallOnScreen { statusBarContent = .call(strongSelf, call.context.account, call) } else if let groupCall = groupCall, !hasGroupCallOnScreen { statusBarContent = .groupCall(strongSelf, groupCall.account, groupCall) @@ -1021,7 +1055,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { if callController.isNodeLoaded { mainWindow.hostView.containerView.endEditing(true) if callController.view.superview == nil { - mainWindow.present(callController, on: .calls) + if useFlatModalCallsPresentation(context: callController.call.context) { + (mainWindow.viewController as? NavigationController)?.pushViewController(callController) + } else { + mainWindow.present(callController, on: .calls) + } } else { callController.expandFromPipIfPossible() } @@ -1179,30 +1217,94 @@ public final class SharedAccountContextImpl: SharedAccountContext { return } - if let currentCallController = self.callController { - if currentCallController.call == .call(call) { - self.navigateToCurrentCall() - return - } else { + if let conferenceCall = call.conferenceCall { + if let groupCallController = self.groupCallController { + if groupCallController.call === conferenceCall { + return + } + groupCallController.dismiss(closing: true, manual: false) + self.groupCallController = nil + } + var transitioniongCallController: CallController? + if let callController = self.callController { + transitioniongCallController = callController + callController.dismissWithoutAnimation() self.callController = nil - currentCallController.dismiss() } - } - - self.mainWindow?.hostView.containerView.endEditing(true) - let callController = CallController(sharedContext: self, account: call.context.account, call: .call(call), easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) - self.callController = callController - callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in - guard let self, let callController else { - completion(false) - return + + let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: conferenceCall.accountContext, call: conferenceCall) + |> deliverOnMainQueue).start(next: { [weak self, weak transitioniongCallController] initialData in + guard let self else { + return + } + guard let navigationController = self.mainWindow?.viewController as? NavigationController else { + return + } + guard let call = self.call, let conferenceCall = call.conferenceCall else { + return + } + + let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: conferenceCall.accountContext, call: conferenceCall, initialData: initialData, sourceCallController: transitioniongCallController) + 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 { + if let currentCallController = self.callController { + if currentCallController.call === call { + self.navigateToCurrentCall() + return + } else { + self.callController = nil + currentCallController.dismiss() + } } - if callController.window == nil { + + self.mainWindow?.hostView.containerView.endEditing(true) + let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) + self.callController = callController + callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in + guard let self, let callController else { + completion(false) + return + } + if callController.window == nil { + if useFlatModalCallsPresentation(context: callController.call.context) { + (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) + } else { + self.mainWindow?.present(callController, on: .calls) + } + } + completion(true) + } + callController.onViewDidAppear = { [weak self] in + if let self { + self.hasGroupCallOnScreenPromise.set(true) + } + } + callController.onViewDidDisappear = { [weak self] in + if let self { + self.hasGroupCallOnScreenPromise.set(false) + } + } + if useFlatModalCallsPresentation(context: callController.call.context) { + self.hasGroupCallOnScreenPromise.set(true) + (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) + } else { self.mainWindow?.present(callController, on: .calls) } - completion(true) } - self.mainWindow?.present(callController, on: .calls) } public func updateNotificationTokensRegistration() { @@ -1434,7 +1536,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { if let callController = self.callController { if callController.isNodeLoaded && callController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) - mainWindow.present(callController, on: .calls) + + if useFlatModalCallsPresentation(context: callController.call.context) { + (mainWindow.viewController as? NavigationController)?.pushViewController(callController) + } else { + mainWindow.present(callController, on: .calls) + } } } else if let groupCallController = self.groupCallController { if groupCallController.isNodeLoaded && groupCallController.view.superview == nil { @@ -3135,3 +3242,10 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation } return nil } + +private func useFlatModalCallsPresentation(context: AccountContext) -> Bool { + if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_modalcalls"] != nil { + return false + } + return true +} diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index fd77090a73..226229813e 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -755,7 +755,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } public func openAvatars() { - self.accountSettingsController?.openAvatars() + if let accountSettingsController = self.accountSettingsController { + self.rootTabController?.updateControllerLayout(controller: accountSettingsController) + accountSettingsController.openAvatars() + } } } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index c894a82f00..ec477ff08c 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -86,7 +86,6 @@ public struct ApplicationSpecificItemCacheCollectionId { public static let instantPageStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.instantPageStoredState.rawValue) public static let cachedInstantPages = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedInstantPages.rawValue) public static let cachedWallpapers = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedWallpapers.rawValue) - public static let mediaPlaybackStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaPlaybackStoredState.rawValue) public static let cachedGeocodes = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedGeocodes.rawValue) public static let visualMediaStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.visualMediaStoredState.rawValue) public static let cachedImageRecognizedContent = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedImageRecognizedContent.rawValue) diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index ab815d0e7f..843dee34df 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/TgVoipWebrtc:TgVoipWebrtc", "//submodules/FFMpegBinding", "//submodules/ManagedFile", + "//submodules/AppBundle", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 94b9cd8825..72d5365a1a 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -7,6 +7,12 @@ import TelegramUIPreferences import TgVoip import TgVoipWebrtc +#if os(iOS) +import UIKit +import AppBundle +import Accelerate +#endif + private func debugUseLegacyVersionForReflectors() -> Bool { #if DEBUG && false return true @@ -407,8 +413,179 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { } } +#if targetEnvironment(simulator) +private extension UIImage { + @available(iOS 13.0, *) + func toBiplanarYUVPixelBuffer() -> CVPixelBuffer? { + guard let cgImage = self.cgImage else { + return nil + } + + // Dimensions + let width = Int(self.size.width * self.scale) + let height = Int(self.size.height * self.scale) + + // 1) Create an ARGB8888 vImage buffer from the UIImage (CGImage). + // We will first allocate a buffer for ARGB pixels, then use + // vImage to copy cgImage → argbBuffer. + + // Each ARGB pixel is 4 bytes + let argbBytesPerPixel = 4 + let argbRowBytes = width * argbBytesPerPixel + + // Allocate contiguous memory for ARGB data + let argbData = malloc(argbRowBytes * height) + defer { + free(argbData) + } + + // Create a vImage buffer for ARGB + var argbBuffer = vImage_Buffer( + data: argbData, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: argbRowBytes + ) + + // Initialize the ARGB buffer from our CGImage + // This helper function can fail, so check the result: + var format = vImage_CGImageFormat( + bitsPerComponent: 8, + bitsPerPixel: 32, + colorSpace: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue), + renderingIntent: CGColorRenderingIntent.defaultIntent + )! + + if vImageBuffer_InitWithCGImage( + &argbBuffer, + &format, + nil, + cgImage, + vImage_Flags(kvImageNoFlags) + ) != kvImageNoError { + return nil + } + + // 2) Create a CVPixelBuffer in YUV 420 (bi-planar) format. + // Typically, you’d choose either kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + // or kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange. + + let pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + let attrs: [CFString: Any] = [ + kCVPixelBufferIOSurfacePropertiesKey: [:], + // Optionally, specify other attributes if needed. + ] + + var cvPixelBufferOut: CVPixelBuffer? + guard CVPixelBufferCreate( + kCFAllocatorDefault, + width, + height, + pixelFormat, + attrs as CFDictionary, + &cvPixelBufferOut + ) == kCVReturnSuccess, + let pixelBuffer = cvPixelBufferOut + else { + return nil + } + + // 3) Lock the CVPixelBuffer to get direct access to its planes. + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + // Plane 0: Y-plane + guard let yBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return nil + } + let yPitch = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) + + // Plane 1: CbCr-plane + guard let cbcrBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return nil + } + let cbcrPitch = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) + + // 4) Create vImage buffers for each plane. + + // Y plane is full size (width x height) + var yBuffer = vImage_Buffer( + data: yBaseAddress, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: yPitch + ) + + // CbCr plane is half height, but each row has interleaved Cb/Cr + // so the plane is (width/2) * 2 bytes = width bytes wide, and height/2. + var cbcrBuffer = vImage_Buffer( + data: cbcrBaseAddress, + height: vImagePixelCount(height / 2), + width: vImagePixelCount(width), + rowBytes: cbcrPitch + ) + + var info = vImage_ARGBToYpCbCr() + var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 0, CbCr_bias: 128, YpRangeMax: 255, CbCrRangeMax: 255, YpMax: 255, YpMin: 1, CbCrMax: 255, CbCrMin: 0) + vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0) + + let error = vImageConvert_ARGB8888To420Yp8_CbCr8( + &argbBuffer, + &yBuffer, + &cbcrBuffer, + &info, + nil, + UInt32(kvImageDoNotTile) + ) + + if error != kvImageNoError { + return nil + } + + return pixelBuffer + } + + @available(iOS 13.0, *) + var cmSampleBuffer: CMSampleBuffer? { + guard let pixelBuffer = self.toBiplanarYUVPixelBuffer() else { + return nil + } + var newSampleBuffer: CMSampleBuffer? = nil + + var timingInfo = CMSampleTimingInfo( + duration: CMTimeMake(value: 1, timescale: 30), + presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), + decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) + ) + + var videoInfo: CMVideoFormatDescription? = nil + CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &videoInfo) + guard let videoInfo = videoInfo else { + return nil + } + CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: videoInfo, sampleTiming: &timingInfo, sampleBufferOut: &newSampleBuffer) + + if let newSampleBuffer = newSampleBuffer { + let attachments = CMSampleBufferGetSampleAttachmentsArray(newSampleBuffer, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + } + + return newSampleBuffer + } +} +#endif + public final class OngoingCallVideoCapturer { internal let impl: OngoingCallThreadLocalContextVideoCapturer + + #if targetEnvironment(simulator) + private var simulatedVideoTimer: Foundation.Timer? + #endif private let isActivePromise = ValuePromise(true, ignoreRepeated: true) public var isActive: Signal { @@ -419,13 +596,47 @@ public final class OngoingCallVideoCapturer { if isCustom { self.impl = OngoingCallThreadLocalContextVideoCapturer.withExternalSampleBufferProvider() } else { + #if targetEnvironment(simulator) && false + self.impl = OngoingCallThreadLocalContextVideoCapturer.withExternalSampleBufferProvider() + let imageSize = CGSize(width: 600.0, height: 800.0) + UIGraphicsBeginImageContextWithOptions(imageSize, true, 1.0) + let sourceImage: UIImage? + let imagePath = NSTemporaryDirectory() + "frontCameraImage.jpg" + if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) { + sourceImage = image + } else { + sourceImage = UIImage(bundleImageName: "Camera/SelfiePlaceholder")! + } + if let sourceImage { + sourceImage.draw(in: CGRect(origin: CGPoint(), size: imageSize)) + } + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + self.simulatedVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + if #available(iOS 13.0, *) { + if let image, let sampleBuffer = image.cmSampleBuffer { + self.injectSampleBuffer(sampleBuffer, rotation: .up, completion: {}) + } + } + }) + #else self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: "", keepLandscape: keepLandscape) + #endif } let isActivePromise = self.isActivePromise self.impl.setOnIsActiveUpdated({ value in isActivePromise.set(value) }) } + + deinit { + #if targetEnvironment(simulator) + self.simulatedVideoTimer?.invalidate() + #endif + } public func switchVideoInput(isFront: Bool) { self.impl.switchVideoInput(isFront ? "" : "back") diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index e3e408f75d..421d23ead1 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit e3e408f75d03c9f3961c9a0dea0fdc7101708286 +Subproject commit 421d23ead1cf8595fb72087a01d7b1452ffd2723 diff --git a/third-party/flatc/BUILD b/third-party/flatc/BUILD index 503240e9ea..e3d54ce41c 100644 --- a/third-party/flatc/BUILD +++ b/third-party/flatc/BUILD @@ -22,14 +22,14 @@ set -x pushd "$$BUILD_DIR/flatbuffers-24.12.23" mkdir build cd build - PATH="$$PATH:$$CMAKE_DIR/cmake-3.23.1-macos-universal/CMake.app/Contents/bin" cmake .. -DCMAKE_BUILD_TYPE=Release" + PATH="$$PATH:$$CMAKE_DIR/cmake-3.23.1-macos-universal/CMake.app/Contents/bin" cmake .. -DCMAKE_BUILD_TYPE=Release -DFLATBUFFERS_BUILD_TESTS=0 -DFLATBUFFERS_INSTALL=0 -DFLATBUFFERS_BUILD_FLATLIB=0 -DFLATBUFFERS_STATIC_FLATC=0 make -j $$core_count popd - tar -cf "$(location flatc.tar)" -C "$$BUILD_DIR/flatbuffers-24.12.23/build" . + cp "$$BUILD_DIR/flatbuffers-24.12.23/build/flatc" "$(location flatc_bin)" """, outs = [ - "flatc.tar", + "flatc_bin", ], visibility = [ "//visibility:public",