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

This commit is contained in:
Ilya Laktyushin 2025-01-22 13:37:05 +04:00
commit bf8c4f1ecd
107 changed files with 5958 additions and 1013 deletions

View File

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

View File

@ -143,6 +143,9 @@ public protocol PresentationCall: AnyObject {
var state: Signal<PresentationCallState, NoError> { get }
var audioLevel: Signal<Float, NoError> { get }
var hasConference: Signal<Bool, NoError> { get }
var conferenceCall: PresentationGroupCall? { get }
var isMuted: Signal<Bool, NoError> { 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)
}

View File

@ -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<ShareControllerExternalStatus, ShareControllerError>)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<MediaPlaybackStoredState?, NoError> {
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<Never, NoError> {
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
})
}

View File

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

View File

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

View File

@ -46,6 +46,7 @@ swift_library(
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/ChatPresentationInterfaceState",
"//submodules/CheckNode",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Bool, NoError> {
switch self {
case let .call(call):
return call.isMuted
case let .groupCall(groupCall):
return groupCall.isMuted
}
}
public var audioLevel: Signal<Float, NoError> {
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<Bool, NoError> {
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<PeerView?, NoError>
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() {

View File

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

View File

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

View File

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

View File

@ -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<Bool>(false)
private var hasConferenceValue: Bool = false {
didSet {
if self.hasConferenceValue != oldValue {
self.hasConferencePromise.set(self.hasConferenceValue)
}
}
}
public var hasConference: Signal<Bool, NoError> {
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<Bool>(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<String?>()
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<String?>()
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<Bool, NoError> {
let debugLogValue = Promise<String?>()
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<String?>()
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/Utils/DarwinDirStat",
"//submodules/Emoji",
"//submodules/TelegramCore/FlatSerialization",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -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<Bytes: ContiguousBytes>(
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<UInt8>.size &* padding)
}
/// Adds an array of type Scalar to the buffer memory
/// - Parameter elements: An array of Scalars
@inline(__always)
@usableFromInline
mutating func push<T: Scalar>(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<T: NativeStruct>(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<T: NativeStruct>(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<T: Scalar>(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<String.UTF8View.Element>,
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<T>(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<T>.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<T>(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<T>(
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<UInt8>(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<Int32>.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) }
"""
}
}

View File

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

View File

@ -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<T>(
_ 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
}

View File

@ -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<UOffset>.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<UOffset>.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<VOffset>.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<T: Scalar>(len: Int, type: T.Type) {
preAlign(len: len, alignment: MemoryLayout<T>.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<UOffset>.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<T: Scalar>(_ 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<T: Scalar>(
_ elements: [T],
size: Int) -> Offset
{
let size = size
startVector(size, elementSize: MemoryLayout<T>.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<UInt8>.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<T: Enum>(_ 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<T: Enum>(
_ 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<Offset>.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<T: NativeStruct>(ofStructs structs: [T])
-> Offset
{
startVector(
structs.count * MemoryLayout<T>.size,
elementSize: MemoryLayout<T>.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<T: NativeStruct>(
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<T: NativeStruct>(
struct s: T) -> Offset
{
let size = MemoryLayout<T>.size
preAlign(len: size, alignment: MemoryLayout<T>.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<T: Scalar>(
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<T: Scalar>(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<T: Scalar>(element: T) -> UOffset {
let size = MemoryLayout<T>.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<FieldLoc>.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)
}
}
}

View File

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

View File

@ -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<Int32>.size)
}
}

View File

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

View File

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

View File

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

View File

@ -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<T: Scalar>(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<T: Scalar>(_ 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<T: Scalar>(_ 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<T: Scalar>(_ 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<T: Scalar>(_ value: T, index: Int32) -> Bool {
mutate(value: value, o: index)
}
}
extension Struct: Mutable {}
extension Table: Mutable {}

View File

@ -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<T: ObjectAPIPacker>(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<T: ObjectAPIPacker>(
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
}
}

View File

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

View File

@ -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<T: FlatBufferObject & Verifiable>(
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<T: FlatBufferObject & Verifiable>(
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<T: FlatBufferObject>(
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<T: FlatBufferObject & Verifiable>(
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<T>.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<T: FlatBufferObject>(byteBuffer: inout ByteBuffer) -> T {
T.init(
byteBuffer,
o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) +
Int32(byteBuffer.reader))
}

View File

@ -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<T>(
_ 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<Int32>.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<T: ObjectAPIPacker>(type: T.Type) -> ByteBuffer
where T.T == Self
{
fatalError("serialize should never be called from string directly")
}
public func serialize<T: ObjectAPIPacker>(
builder: inout FlatBufferBuilder,
type: T.Type) -> ByteBuffer where T.T == Self
{
fatalError("serialize should never be called from string directly")
}
}

View File

@ -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<T: Scalar>(of type: T.Type, at o: Int32) -> T {
let r = bb.read(def: T.self, position: Int(o + position))
return r
}
}

View File

@ -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<Int32>.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<T>(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<T>(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<T: FlatbuffersInitializable>(_ 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<T: FlatbuffersInitializable>(_ 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<T>(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<Int32>.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<Int32>.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..<minValue {
let b = fbb.read(def: Int8.self, position: Int(i + startPos1))
let byte = key[Int(i)]
if b != byte {
return Int32(b - Int8(byte))
}
}
return len1 - len2
}
}

View File

@ -0,0 +1,203 @@
/*
* 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
/// `TableVerifier` verifies a table object is within a provided memory.
/// It checks if all the objects for a specific generated table, are within
/// the bounds of the buffer, aligned.
public struct TableVerifier {
/// position of current table in `ByteBuffer`
fileprivate var _position: Int
/// Current VTable position
fileprivate var _vtable: Int
/// Length of current VTable
fileprivate var _vtableLength: Int
/// `Verifier` object created in the base verifable call.
fileprivate var _verifier: Verifier
/// Creates a `TableVerifier` verifier that allows the Flatbuffer object
/// to verify the buffer before accessing any of the data.
///
/// - Parameters:
/// - position: Current table Position
/// - vtable: Current `VTable` position
/// - vtableLength: Current `VTable` length
/// - verifier: `Verifier` Object that caches the data of the verifiable object
internal init(
position: Int,
vtable: Int,
vtableLength: Int,
verifier: inout Verifier)
{
_position = position
_vtable = vtable
_vtableLength = vtableLength
_verifier = verifier
}
/// Dereference the current object position from the `VTable`
/// - Parameter field: Current VTable refrence to position.
/// - Throws: A `FlatbuffersErrors` incase the voffset is not aligned/outOfBounds/apparentSizeTooLarge
/// - Returns: An optional position for current field
internal mutating func dereference(_ field: VOffset) throws -> 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<T>(
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<T>(
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<T>(
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<T>.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()
}
}

View File

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

View File

@ -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<T>(
_ 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<T>(
_ 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<Int32>.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<T>(
_ 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<U>: 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<T>(
_ 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<U, S>: 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<T>(
_ 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<S>.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<Int32>.size)),
by: MemoryLayout<UOffset>.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<S> 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<S.T>.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<UOffset>.size * count
try completion(&verifier, _enum, offsetsRange.start + fieldIndex)
count += 1
}
}
}

View File

@ -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<T>(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<T>.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<T>(position: Int, of type: T.Type) throws {
try isAligned(position: position, type: type)
try rangeInBuffer(position: position, size: MemoryLayout<T>.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<T>(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<Int32>.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
}
}
}

View File

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

View File

@ -0,0 +1,6 @@
namespace TelegramCore;
struct MediaId {
namespace: int;
id: int64;
}

View File

@ -0,0 +1,6 @@
namespace TelegramCore;
struct PixelDimensions {
width: int;
height: int;
}

View File

@ -0,0 +1,11 @@
include "MediaId.fbs";
include "VideoThumbnail.fbs";
namespace TelegramCore;
table TelegramMediaFile {
id: MediaId;
videoThumbnails: [VideoThumbnail];
}
root_type TelegramMediaFile;

View File

@ -0,0 +1,7 @@
include "PixelDimensions.fbs";
namespace TelegramCore;
struct VideoThumbnail {
dimensions: PixelDimensions;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -229,7 +229,19 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
}
|> mapToSignal { validatedResource -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> 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<UploadedMediaThumbnailResult, PendingMessageUploadError> = .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<PendingMessageUploadedContentResult, PendingMessageUploadError> 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<PendingMessageUploadedContentResult, PendingMessageUploadError> 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<PendingMessageUploadedContentResult, PendingMessageUploadError> 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import Foundation
import Foundation
import Postbox
private let typeFileName: Int32 = 0

View File

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

View File

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

View File

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

View File

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

View File

@ -589,6 +589,28 @@ public extension TelegramEngine {
|> ignoreValues
}
public func updateLocallyDerivedData(messageId: MessageId, update: @escaping ([String: CodableEntry]) -> [String: CodableEntry]) -> Signal<Never, NoError> {
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<Never, NoError> {
return _internal_rateAudioTranscription(postbox: self.account.postbox, network: self.account.network, messageId: messageId, id: id, isGood: isGood)
}

View File

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

View File

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

View File

@ -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<ChannelAddressNameAssignmentAvailability, NoError> {
return _internal_channelAddressNameAssignmentAvailability(account: self.account, peerId: peerId)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/Utils/RangeSet",
"//submodules/MediaResources",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -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<Empty>()
private let peerSelectorButton = ComponentView<Empty>()
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
@ -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<ViewControllerComponentContainer.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<EnginePeer?, NoError> = .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<Empty>()
private var avatarNode: AvatarNode?
private var selectorIcon: ComponentView<Empty>?
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<Empty>, 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<Empty>
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<Empty>, 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)
}
}

View File

@ -100,6 +100,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let openBotButton = ComponentView<Empty>()
private var dailyRevenueText: ComponentView<Empty>?
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<Empty>] = []
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(

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "arrows.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More