diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift index 3281342e1f..19281cc1fc 100644 --- a/submodules/AccountContext/Sources/ShareController.swift +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -49,12 +49,20 @@ public enum ShareControllerError { } public enum ShareControllerSubject { + public final class MediaParameters { + public let startAtTimestamp: Int32? + + public init(startAtTimestamp: Int32?) { + self.startAtTimestamp = startAtTimestamp + } + } + case url(String) case text(String) case quote(text: String, url: String) case messages([Message]) case image([ImageRepresentationWithReference]) - case media(AnyMediaReference) + case media(AnyMediaReference, MediaParameters?) case mapMedia(TelegramMediaMap) case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal) } diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index f4fc4b03ff..0a46ee4a66 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -569,10 +569,10 @@ public class BrowserScreen: ViewController, MinimizableController { var isDocument = false if let content = self.content.last { if let documentContent = content as? BrowserDocumentContent { - subject = .media(documentContent.file.abstract) + subject = .media(documentContent.file.abstract, nil) isDocument = true } else if let documentContent = content as? BrowserPdfContent { - subject = .media(documentContent.file.abstract) + subject = .media(documentContent.file.abstract, nil) isDocument = true } else { subject = .url(url) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index ba10231f7e..b8056bc807 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -759,7 +759,7 @@ open class NavigationController: UINavigationController, ContainableController, } let effectiveModalTransition: CGFloat - if visibleModalCount == 0 || navigationLayout.modal[i].isFlat { + if visibleModalCount == 0 || (navigationLayout.modal[i].isFlat && !navigationLayout.modal[i].flatReceivesModalTransition) { effectiveModalTransition = 0.0 } else if visibleModalCount == 1 { effectiveModalTransition = 1.0 - topModalDismissProgress diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index a2a123c073..a4fddcfcc8 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -11,6 +11,7 @@ enum RootNavigationLayout { struct ModalContainerLayout { var controllers: [ViewController] var isFlat: Bool + var flatReceivesModalTransition: Bool var isStandalone: Bool } @@ -26,6 +27,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL let requiresModal: Bool var beginsModal: Bool = false var isFlat: Bool = false + var flatReceivesModalTransition: Bool = false var isStandalone: Bool = false switch controller.navigationPresentation { case .default: @@ -39,6 +41,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL requiresModal = true beginsModal = true isFlat = true + flatReceivesModalTransition = controller.flatReceivesModalTransition case .standaloneModal: requiresModal = true beginsModal = true @@ -68,7 +71,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL if requiresModal { controller._presentedInModal = true if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone { - modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) + modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone)) } else { modalStack[modalStack.count - 1].controllers.append(controller) } @@ -78,7 +81,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL controller._presentedInModal = true } if modalStack[modalStack.count - 1].isStandalone { - modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) + modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone)) } else { modalStack[modalStack.count - 1].controllers.append(controller) } diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 5e09c77e07..3a86497e78 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -342,9 +342,16 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.validLayout = layout var isStandaloneModal = false - if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation { - isStandaloneModal = true + var flatReceivesModalTransition = false + if let controller = controllers.first { + if case .standaloneModal = controller.navigationPresentation { + isStandaloneModal = true + } + if controller.flatReceivesModalTransition { + flatReceivesModalTransition = true + } } + let _ = flatReceivesModalTransition transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) self.ignoreScrolling = true @@ -378,7 +385,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes } else { self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) } - if isStandaloneModal || isLandscape || self.isFlat { + if isStandaloneModal || isLandscape || (self.isFlat && !flatReceivesModalTransition) { self.container.cornerRadius = 0.0 } else { self.container.cornerRadius = 10.0 @@ -419,13 +426,19 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size) let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition - let maxScaledTopInset: CGFloat = topInset - 10.0 + var maxScaledTopInset: CGFloat = topInset - 10.0 + if flatReceivesModalTransition { + maxScaledTopInset = 0.0 + if let statusBarHeight = layout.statusBarHeight { + maxScaledTopInset += statusBarHeight + } + } let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) } } else { self.panRecognizer?.isEnabled = false - if self.isFlat { + if self.isFlat && !flatReceivesModalTransition { self.dim.backgroundColor = .clear self.container.clipsToBounds = true self.container.cornerRadius = 0.0 diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index aaf2778854..90c53ba32c 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -163,6 +163,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open var navigationPresentation: ViewControllerNavigationPresentation = .default open var _presentedInModal: Bool = false + open var flatReceivesModalTransition: Bool = false public var presentedOverCoveringView: Bool = false diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 49e2103cc1..5d1067eb15 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -186,6 +186,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var interacting: ((Bool) -> Void)? + var shareMediaParameters: (() -> ShareControllerSubject.MediaParameters?)? + private var seekTimer: SwiftSignalKit.Timer? private var currentIsPaused: Bool = true private var seekRate: Double = 1.0 @@ -1644,16 +1646,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } else { if let file = content.file { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: file)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), nil) preferredAction = .saveToCameraRoll } else if let image = content.image { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: image)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), nil) preferredAction = .saveToCameraRoll actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved } } } else if let file = m as? TelegramMediaFile { - subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file)) + subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file), strongSelf.shareMediaParameters?()) if file.isAnimated { if messages[0].id.peerId.namespace == Namespaces.Peer.SecretChat { preferredAction = .default @@ -1666,7 +1668,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controllerInteraction = strongSelf.controllerInteraction let _ = (toggleGifSaved(account: context.account, fileReference: .message(message: MessageReference(message._asMessage()), media: file), saved: true) - |> deliverOnMainQueue).start(next: { result in + |> deliverOnMainQueue).start(next: { result in switch result { case .generic: controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) @@ -1873,7 +1875,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } var preferredAction = ShareControllerPreferredAction.default - var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media)) + var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media), self.shareMediaParameters?()) if let file = media as? TelegramMediaFile { if file.isAnimated { @@ -1935,10 +1937,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } else { if let file = content.file { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: file)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), self.shareMediaParameters?()) preferredAction = .saveToCameraRoll } else if let image = content.image { - subject = .media(.webPage(webPage: WebpageReference(webpage), media: image)) + subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), self.shareMediaParameters?()) preferredAction = .saveToCameraRoll } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 5d47d50928..71fcebb2df 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1404,6 +1404,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.clipsToBounds = true + 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 +1853,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { disablePictureInPicture = true } else if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.namespace == Namespaces.Message.Local { disablePictureInPicture = true - } else { + } + + if message.paidContent == nil { let throttledSignal = videoNode.status |> mapToThrottled { next -> Signal in return .single(next) |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue())) @@ -2390,6 +2403,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { seek = .timecode(time) } } + + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { + for attribute in message.attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + seek = .timecode(Double(attribute.timestamp)) + } + } + } } videoNode.setBaseRate(self.playbackRate ?? 1.0) diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift index e55f6ffdea..9b419ded63 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift @@ -147,7 +147,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { @objc func actionButtonPressed() { if let shareMedia = self.shareMedia { - self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil) + self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia, nil), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil) } } } diff --git a/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift b/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift index 69ec0dcfc3..bdb545a9e2 100644 --- a/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift +++ b/submodules/MediaResources/Sources/MediaPlaybackStoredState.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import TelegramCore import TelegramUIPreferences +import Postbox public final class MediaPlaybackStoredState: Codable { public let timestamp: Double @@ -29,28 +30,28 @@ public final class MediaPlaybackStoredState: Codable { } public func mediaPlaybackStoredState(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal { - let key = EngineDataBuffer(length: 20) - key.setInt32(0, value: messageId.namespace) - key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value()) - key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value()) - key.setInt32(16, value: messageId.id) - - return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key)) - |> map { entry -> MediaPlaybackStoredState? in - return entry?.get(MediaPlaybackStoredState.self) + return engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> map { message -> MediaPlaybackStoredState? in + guard let message else { + return nil + } + for attribute in message.attributes { + if let attribute = attribute as? DerivedDataMessageAttribute { + return attribute.data["mps"]?.get(MediaPlaybackStoredState.self) + } + } + return nil } } public func updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine, messageId: EngineMessage.Id, state: MediaPlaybackStoredState?) -> Signal { - let key = EngineDataBuffer(length: 20) - key.setInt32(0, value: messageId.namespace) - key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value()) - key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value()) - key.setInt32(16, value: messageId.id) - - if let state = state { - return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key, item: state) - } else { - return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key) - } + return engine.messages.updateLocallyDerivedData(messageId: messageId, update: { data in + var data = data + if let state, let entry = CodableEntry(state) { + data["mps"] = entry + } else { + data.removeValue(forKey: "mps") + } + return data + }) } diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index de69497b10..3d29daa132 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -188,7 +188,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { var actionCompletionText: String? if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) - subject = .media(videoFileReference.abstract) + subject = .media(videoFileReference.abstract, nil) actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved } else { subject = .image(entry.representations) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index d000c3da2b..2713ca2a34 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -521,7 +521,7 @@ public final class ThemePreviewController: ViewController { subject = .url("https://t.me/addtheme/\(slug)") preferredAction = .default case let .media(media): - subject = .media(media) + subject = .media(media, nil) preferredAction = .default } let controller = ShareController(context: self.context, subject: subject, preferredAction: preferredAction) diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 3f541667da..f20bc90047 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramUI/Components/MessageInputPanelComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/ChatPresentationInterfaceState", + "//submodules/CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareActionButtonNode.swift b/submodules/ShareController/Sources/ShareActionButtonNode.swift index 8be8ba0a98..c63b3df2ae 100644 --- a/submodules/ShareController/Sources/ShareActionButtonNode.swift +++ b/submodules/ShareController/Sources/ShareActionButtonNode.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import ContextUI +import CheckNode public final class ShareActionButtonNode: HighlightTrackingButtonNode { private let referenceNode: ContextReferenceContentNode @@ -109,3 +110,74 @@ public final class ShareActionButtonNode: HighlightTrackingButtonNode { self.referenceNode.frame = self.bounds } } + +public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode { + private let checkNode: CheckNode + private let titleTextNode: TextNode + + public var titleTextColor: UIColor { + didSet { + self.setNeedsLayout() + } + } + public var checkNodeTheme: CheckNodeTheme { + didSet { + self.checkNode.theme = self.checkNodeTheme + } + } + + private let titleText: String + + public var value: Bool { + return self.checkNode.selected + } + + public init(titleText: String, titleTextColor: UIColor, checkNodeTheme: CheckNodeTheme) { + self.titleText = titleText + self.titleTextColor = titleTextColor + self.checkNodeTheme = checkNodeTheme + + self.checkNode = CheckNode(theme: checkNodeTheme, content: .check) + self.checkNode.isUserInteractionEnabled = false + + self.titleTextNode = TextNode() + self.titleTextNode.isUserInteractionEnabled = false + self.titleTextNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.checkNode) + self.addSubnode(self.titleTextNode) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.checkNode.setSelected(!self.checkNode.selected, animated: true) + } + + override public func layout() { + super.layout() + + if self.bounds.width < 1.0 { + return + } + + let checkSize: CGFloat = 18.0 + let checkSpacing: CGFloat = 10.0 + + let (titleTextLayout, titleTextApply) = TextNode.asyncLayout(self.titleTextNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.titleText, font: Font.regular(13.0), textColor: self.titleTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: self.bounds.width - 8.0 * 2.0 - checkSpacing - checkSize, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let _ = titleTextApply() + + let contentWidth = checkSize + checkSpacing + titleTextLayout.size.width + let checkFrame = CGRect(origin: CGPoint(x: floor((self.bounds.width - contentWidth) * 0.5), y: floor((self.bounds.height - checkSize) * 0.5)), size: CGSize(width: checkSize, height: checkSize)) + + let isFirstTime = self.checkNode.bounds.isEmpty + self.checkNode.frame = checkFrame + if isFirstTime { + self.checkNode.setSelected(false, animated: false) + } + + self.titleTextNode.frame = CGRect(origin: CGPoint(x: checkFrame.maxX + checkSpacing, y: floor((self.bounds.height - titleTextLayout.size.height) * 0.5)), size: titleTextLayout.size) + } +} diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 27bb68504b..ff18c24765 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -524,7 +524,7 @@ public final class ShareController: ViewController { self?.actionCompleted?() }) } - case let .media(mediaReference): + case let .media(mediaReference, _): var canSave = false var isVideo = false if mediaReference.media is TelegramMediaImage { @@ -668,7 +668,12 @@ public final class ShareController: ViewController { fromPublicChannel = true } - self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in + var mediaParameters: ShareControllerSubject.MediaParameters? + if case let .media(_, parameters) = self.subject { + mediaParameters = parameters + } + + self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, mediaParameters: mediaParameters, requestLayout: { [weak self] transition in self?.requestLayout(transition: transition) }, presentError: { [weak self] title, text in guard let strongSelf = self else { @@ -765,7 +770,7 @@ public final class ShareController: ViewController { return false } } - case let .media(mediaReference): + case let .media(mediaReference, _): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -981,7 +986,7 @@ public final class ShareController: ViewController { case let .image(representations): let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: .standalone(media: media))) - case let .media(mediaReference): + case let .media(mediaReference, _): collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: mediaReference)) case let .mapMedia(media): let latLong = "\(media.latitude),\(media.longitude)" @@ -1518,7 +1523,7 @@ public final class ShareController: ViewController { messages: messages )) } - case let .media(mediaReference): + case let .media(mediaReference, _): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -2041,7 +2046,7 @@ public final class ShareController: ViewController { messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } - case let .media(mediaReference): + case let .media(mediaReference, mediaParameters): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -2116,7 +2121,15 @@ public final class ShareController: ViewController { if !text.isEmpty && !sendTextAsCaption { messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + var attributes: [MessageAttribute] = [] + if let startAtTimestamp = mediaParameters?.startAtTimestamp, let startAtTimestampNode = strongSelf.controllerNode.startAtTimestampNode, startAtTimestampNode.value { + attributes.append(ForwardVideoTimestampAttribute(timestamp: startAtTimestamp)) + } + if case let .message(message, _) = mediaReference, let sourceMessageId = message.id, (sourceMessageId.peerId.namespace == Namespaces.Peer.CloudUser || sourceMessageId.peerId.namespace == Namespaces.Peer.CloudGroup || sourceMessageId.peerId.namespace == Namespaces.Peer.CloudChannel) { + messages.append(.forward(source: sourceMessageId, threadId: threadId, grouping: .auto, attributes: attributes, correlationId: nil)) + } else { + messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index b3fb9e33ed..0da8a2d193 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -14,6 +14,7 @@ import MultilineTextComponent import TelegramStringFormatting import BundleIconComponent import LottieComponent +import CheckNode enum ShareState { case preparing(Bool) @@ -296,6 +297,22 @@ private final class ShareContentInfoView: UIView { } } +private func textForTimeout(value: Int32) -> String { + if value < 3600 { + let minutes = value / 60 + let seconds = value % 60 + let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutes):\(secondsPadding)\(seconds)" + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let minutesPadding = minutes < 10 ? "0" : "" + let seconds = value % 60 + let secondsPadding = seconds < 10 ? "0" : "" + return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)" + } +} + final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { private weak var controller: ShareController? private let environment: ShareControllerEnvironment @@ -332,6 +349,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let actionsBackgroundNode: ASImageNode private let actionButtonNode: ShareActionButtonNode + let startAtTimestampNode: ShareStartAtTimestampNode? private let inputFieldNode: ShareInputFieldNode private let actionSeparatorNode: ASDisplayNode @@ -366,7 +384,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let showNames = ValuePromise(true) - init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { + init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { self.controller = controller self.environment = environment self.presentationData = presentationData @@ -446,6 +464,13 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.actionButtonNode.titleNode.displaysAsynchronously = false self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + if let startAtTimestamp = mediaParameters?.startAtTimestamp { + //TODO:localize + self.startAtTimestampNode = ShareStartAtTimestampNode(titleText: "Start at \(textForTimeout(value: startAtTimestamp))", titleTextColor: self.presentationData.theme.actionSheet.secondaryTextColor, checkNodeTheme: CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false)) + } else { + self.startAtTimestampNode = nil + } + self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment) self.inputFieldNode.text = presetText ?? "" self.inputFieldNode.preselectText() @@ -650,6 +675,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.contentContainerNode.addSubnode(self.actionsBackgroundNode) self.contentContainerNode.addSubnode(self.inputFieldNode) self.contentContainerNode.addSubnode(self.actionButtonNode) + if let startAtTimestampNode = self.startAtTimestampNode { + self.contentContainerNode.addSubnode(startAtTimestampNode) + } self.inputFieldNode.updateHeight = { [weak self] in if let strongSelf = self { @@ -844,6 +872,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.actionButtonNode.badgeBackgroundColor = presentationData.theme.actionSheet.controlAccentColor self.actionButtonNode.badgeTextColor = presentationData.theme.actionSheet.opaqueItemBackgroundColor + if let startAtTimestampNode = self.startAtTimestampNode { + startAtTimestampNode.titleTextColor = presentationData.theme.actionSheet.secondaryTextColor + startAtTimestampNode.checkNodeTheme = CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false) + } + self.contentNode?.updateTheme(presentationData.theme) } @@ -992,6 +1025,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate actionButtonHeight = buttonHeight bottomGridInset += actionButtonHeight } + if self.startAtTimestampNode != nil { + bottomGridInset += buttonHeight + } let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition) if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil { @@ -1013,6 +1049,10 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + if let startAtTimestampNode = self.startAtTimestampNode { + transition.updateFrame(node: startAtTimestampNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + } + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: inputHeight)), beginWithCurrentState: true) transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)), beginWithCurrentState: true) @@ -1563,6 +1603,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) { return result } + if let startAtTimestampNode = self.startAtTimestampNode { + if let result = startAtTimestampNode.hitTest(startAtTimestampNode.convert(point, from: self), with: event) { + return result + } + } if self.bounds.contains(point) { if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { if let result = contentInfoView.hitTest(self.view.convert(point, to: contentInfoView), with: event) { diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 1d4d9439c0..3b025e3a6a 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -363,7 +363,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentOffsetUpdated = f } - private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) { + private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) { let itemCount = self.entries.count let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) @@ -384,7 +384,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) - let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0) + let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset) return (gridTopInset, itemWidth) } @@ -572,7 +572,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.overrideGridOffsetTransition = nil } - let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset) var scrollToItem: GridNodeScrollToItem? if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 580ab64ff2..a9b0a4f693 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -5560,9 +5560,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, videoTimestamp: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-721186296) + buffer.appendInt32(1836374536) serializeInt32(flags, buffer: buffer, boxed: false) fromPeer.serialize(buffer, true) buffer.appendInt32(481674261) @@ -5580,7 +5580,8 @@ public extension Api.functions.messages { if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 20) != 0 {serializeInt32(videoTimestamp!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("videoTimestamp", String(describing: videoTimestamp))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 9591a7142d..15565d04ea 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -27,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 } @@ -68,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 @@ -82,6 +83,9 @@ public final class CallController: ViewController { public var onViewDidAppear: (() -> Void)? public var onViewDidDisappear: (() -> Void)? + 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 @@ -92,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 @@ -336,12 +346,18 @@ 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 @@ -376,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 @@ -384,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) { @@ -392,7 +412,16 @@ 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 { @@ -422,47 +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.presentingViewController?.dismiss(animated: false, completion: nil) + 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() { - if "".isEmpty { - let _ = self.call.upgradeToConference(completion: { _ in - }) - return - } - - 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 let call = self.call as? PresentationCallImpl else { + guard case let .result(peerIds, _) = result else { + controller?.dismiss() return } - let _ = call.requestAddToConference(peerId: peer.id) - } + if peerIds.isEmpty { + controller?.dismiss() + return + } + + 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.present(controller, in: .current) + self.push(controller) } @objc private func backPressed() { diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 8eff36aac0..7fd1cdc2dd 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -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)? @@ -178,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 @@ -715,6 +717,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 { self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0) self.notifyDismissedInteractivelyOnPanGestureApply = true + self.willBeDismissedInteractively?() self.callScreen.beginPictureInPictureIfPossible() } diff --git a/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift b/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift index 894fcf94d9..ceb75af373 100644 --- a/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift @@ -63,6 +63,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol var back: (() -> Void)? var presentCallRating: ((CallId, Bool) -> Void)? var callEnded: ((Bool) -> Void)? + var willBeDismissedInteractively: (() -> Void)? var dismissedInteractively: (() -> Void)? var present: ((ViewController) -> Void)? var dismissAllTooltips: (() -> Void)? diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 8246e775f7..ad8ac4eeca 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -813,12 +813,6 @@ public final class PresentationCallImpl: PresentationCall { } }) - let upgradedToConferenceCompletions = self.upgradedToConferenceCompletions.copyItems() - self.upgradedToConferenceCompletions.removeAll() - for f in upgradedToConferenceCompletions { - f(conferenceCall) - } - let waitForLocalVideo = self.videoCapturer != nil let waitForRemotePeerId: EnginePeer.Id? = self.peerId @@ -888,6 +882,12 @@ public final class PresentationCallImpl: PresentationCall { return } self.hasConferenceValue = true + + let upgradedToConferenceCompletions = self.upgradedToConferenceCompletions.copyItems() + self.upgradedToConferenceCompletions.removeAll() + for f in upgradedToConferenceCompletions { + f(conferenceCall) + } }) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index 4bbd537ed0..fb4d9b614d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -23,9 +23,6 @@ extension VideoChatScreenComponent.View { guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { return } - guard let peer = self.peer else { - return - } guard let callState = self.callState else { return } @@ -34,7 +31,7 @@ extension VideoChatScreenComponent.View { var items: [ContextMenuItem] = [] - if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { + if self.peer != nil, let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { for peer in displayAsPeers { if peer.peer.id == callState.myPeerId { let avatarSize = CGSize(width: 28.0, height: 28.0) @@ -98,7 +95,7 @@ extension VideoChatScreenComponent.View { }))) var hasPermissions = true - if case let .channel(chatPeer) = peer { + if let peer = self.peer, case let .channel(chatPeer) = peer { if case .broadcast = chatPeer.info { hasPermissions = false } else if chatPeer.flags.contains(.isGigagroup) { @@ -152,54 +149,56 @@ extension VideoChatScreenComponent.View { } } - let qualityList: [(Int, String)] = [ - (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly), - (180, "180p"), - (360, "360p"), - (Int.max, "720p") - ] - - let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" - items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in - return nil - }, action: { [weak self] c, _ in - guard let self else { - c?.dismiss(completion: nil) - return - } + if let members = self.members, members.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) { + let qualityList: [(Int, String)] = [ + (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly), + (180, "180p"), + (360, "360p"), + (Int.max, "720p") + ] - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - - for (quality, title) in qualityList { - let isSelected = self.maxVideoQuality == quality - items.append(.action(ContextMenuActionItem(text: title, icon: { _ in - if isSelected { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - - if self.maxVideoQuality != quality { - self.maxVideoQuality = quality - self.state?.updated(transition: .immediate) - } + let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" + items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in + return nil + }, action: { [weak self] c, _ in + guard let self else { + c?.dismiss(completion: nil) + return + } + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() }))) - } - - c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) - }))) + items.append(.separator) + + for (quality, title) in qualityList { + let isSelected = self.maxVideoQuality == quality + items.append(.action(ContextMenuActionItem(text: title, icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + if self.maxVideoQuality != quality { + self.maxVideoQuality = quality + self.state?.updated(transition: .immediate) + } + }))) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) + }))) + } if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { if component.call.hasScreencast { diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index e2ef92c5d0..3714cf4388 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -205,6 +205,7 @@ private var declaredEncodables: Void = { declareEncodable(WallpaperDataResource.self, f: { WallpaperDataResource(decoder: $0) }) declareEncodable(ForwardOptionsMessageAttribute.self, f: { ForwardOptionsMessageAttribute(decoder: $0) }) declareEncodable(SendAsMessageAttribute.self, f: { SendAsMessageAttribute(decoder: $0) }) + declareEncodable(ForwardVideoTimestampAttribute.self, f: { ForwardVideoTimestampAttribute(decoder: $0) }) declareEncodable(AudioTranscriptionMessageAttribute.self, f: { AudioTranscriptionMessageAttribute(decoder: $0) }) declareEncodable(NonPremiumMessageAttribute.self, f: { NonPremiumMessageAttribute(decoder: $0) }) declareEncodable(TelegramExtendedMedia.self, f: { TelegramExtendedMedia(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index b9286941eb..020ea46c98 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -326,35 +326,34 @@ struct ParsedMessageWebpageAttributes { var isSafe: Bool } -func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?) { +func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?, videoTimestamp: Int32?) { if let media = media { switch media { case let .messageMediaPhoto(flags, photo, ttlSeconds): if let photo = photo { if let mediaImage = telegramMediaImageFromApiPhoto(photo) { - return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil) + return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil, nil) } } else { - return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil) + return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil, nil) } case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId): let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard) - return (mediaContact, nil, nil, nil, nil) + return (mediaContact, nil, nil, nil, nil, nil) case let .messageMediaGeo(geo): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil, nil) case let .messageMediaVenue(geo, title, address, provider, venueId, venueType): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil, nil) case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) - return (mediaMap, nil, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil, nil) case let .messageMediaDocument(flags, document, altDocuments, coverPhoto, videoTimestamp, ttlSeconds): - let _ = videoTimestamp if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments, videoCover: coverPhoto) { - return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) + return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil, videoTimestamp) } } else { var data: TelegramMediaExpiredContentData @@ -365,7 +364,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { data = .file } - return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil) + return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil, nil) } case let .messageMediaWebPage(flags, webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage) { @@ -380,14 +379,14 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI forceLargeMedia: webpageForceLargeMedia, isManuallyAdded: (flags & (1 << 3)) != 0, isSafe: (flags & (1 << 4)) != 0 - )) + ), nil) } case .messageMediaUnsupported: - return (TelegramMediaUnsupported(), nil, nil, nil, nil) + return (TelegramMediaUnsupported(), nil, nil, nil, nil, nil) case .messageMediaEmpty: break case let .messageMediaGame(game): - return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil) + return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil, nil) case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia): var parsedFlags = TelegramMediaInvoiceFlags() if (flags & (1 << 3)) != 0 { @@ -396,7 +395,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI if (flags & (1 << 1)) != 0 { parsedFlags.insert(.shippingAddressRequested) } - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): @@ -421,13 +420,13 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI questionEntities = messageTextEntitiesFromApiEntities(entities) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil, nil) } case let .messageMediaDice(value, emoticon): - return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil) + return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil) case let .messageMediaStory(flags, peerId, id, _): let isMention = (flags & (1 << 1)) != 0 - return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil) + return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil, nil) case let .messageMediaGiveaway(apiFlags, channels, countries, prizeDescription, quantity, months, stars, untilDate): var flags: TelegramMediaGiveaway.Flags = [] if (apiFlags & (1 << 0)) != 0 { @@ -439,9 +438,9 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else if let stars { prize = .stars(amount: stars) } else { - return (nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, nil) } - return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) + return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil) case let .messageMediaGiveawayResults(apiFlags, channelId, additionalPeersCount, launchMsgId, winnersCount, unclaimedCount, winners, months, stars, prizeDescription, untilDate): var flags: TelegramMediaGiveawayResults.Flags = [] if (apiFlags & (1 << 0)) != 0 { @@ -456,15 +455,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else if let stars { prize = .stars(amount: stars) } else { - return (nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, nil) } - return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) + return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil) case let .messageMediaPaidMedia(starsAmount, apiExtendedMedia): - return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil) + return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil, nil) } } - return (nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, nil) } func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { @@ -811,7 +810,7 @@ extension StoreMessage { var consumableContent: (Bool, Bool)? = nil if let media = media { - let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes, videoTimestamp) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) @@ -828,6 +827,10 @@ extension StoreMessage { attributes.append(MediaSpoilerMessageAttribute()) } + if let videoTimestamp { + attributes.append(ForwardVideoTimestampAttribute(timestamp: videoTimestamp)) + } + if mediaValue is TelegramMediaWebpage { let leadingPreview = (flags & (1 << 27)) != 0 diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift index e82718a39b..4a6200ae4b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift @@ -16,8 +16,7 @@ extension TelegramExtendedMedia { } self = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) - if let media = media { + if let media = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId).media { self = .full(media: media) } else { return nil diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index a5ac46ba0b..3f636dc161 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -252,6 +252,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as EffectMessageAttribute: return true + case _ as ForwardVideoTimestampAttribute: + return true default: return false } @@ -950,6 +952,12 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, forwardInfo = nil } } + + for attribute in requestedAttributes { + if attribute is ForwardVideoTimestampAttribute { + attributes.append(attribute) + } + } } else { attributes.append(contentsOf: filterMessageAttributesForOutgoingMessage(sourceMessage.attributes)) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 9cb72f36af..4f5fb28af6 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -229,7 +229,19 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } |> mapToSignal { validatedResource -> Signal in if let validatedResource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = validatedResource.fileReference { - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: 0, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, videoTimestamp: nil, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) + var flags: Int32 = 0 + + var videoTimestamp: Int32? + for attribute in attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp + } + } + if videoTimestamp != nil { + flags |= 1 << 4 + } + + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, videoTimestamp: videoTimestamp, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) } else { return .fail(.generic) } @@ -239,14 +251,18 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post var flags: Int32 = 0 var emojiSearchQuery: String? + var videoTimestamp: Int32? for attribute in attributes { if let attribute = attribute as? EmojiSearchQueryMessageAttribute { emojiSearchQuery = attribute.query flags |= (1 << 1) + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + flags |= (1 << 4) + videoTimestamp = attribute.timestamp } } - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, videoTimestamp: nil, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, videoTimestamp: videoTimestamp, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: false, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) @@ -853,6 +869,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili if !forceReupload, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { var flags: Int32 = 0 var ttlSeconds: Int32? + var videoTimestamp: Int32? if let autoclearMessageAttribute = autoclearMessageAttribute { flags |= 1 << 0 ttlSeconds = autoclearMessageAttribute.timeout @@ -861,12 +878,15 @@ 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 } } 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: nil, 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: nil, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) ) } referenceKey = key @@ -1086,6 +1106,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 @@ -1093,6 +1114,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 } } @@ -1121,12 +1144,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)) } @@ -1137,7 +1164,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapError { _ -> PendingMessageUploadError in } |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { - return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds))) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { @@ -1155,8 +1182,11 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili if let _ = videoCoverPhoto { flags |= (1 << 3) } + if videoTimestamp != nil { + flags |= (1 << 4) + } - let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) + let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) if let _ = ttlSeconds { return .single(result) } else { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 1a147217d9..3532126d7c 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -324,6 +324,7 @@ private func sendUploadedMessageContent( } var replyToStoryId: StoryId? var scheduleTime: Int32? + var videoTimestamp: Int32? var sendAsPeerId: PeerId? var bubbleUpEmojiOrStickersets = false @@ -360,6 +361,9 @@ private func sendUploadedMessageContent( scheduleTime = attribute.scheduleTime } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + flags |= Int32(1 << 20) + videoTimestamp = attribute.timestamp } } @@ -442,7 +446,7 @@ private func sendUploadedMessageContent( } if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index b5d80567dc..8bea175a4a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1159,7 +1159,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: let messageText = text var medias: [Media] = [] - let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes, videoTimestamp) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) @@ -1172,6 +1172,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let expirationTimer = expirationTimer { attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) } + if let videoTimestamp { + attributes.append(ForwardVideoTimestampAttribute(timestamp: videoTimestamp)) + } if let nonPremium = nonPremium, nonPremium { attributes.append(NonPremiumMessageAttribute()) diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 0fe4702964..5f112f6c70 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -152,7 +152,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes forwardInfo = updatedMessage.forwardInfo threadId = updatedMessage.threadId } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { - let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (mediaValue, _, nonPremium, hasSpoiler, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { media = [mediaValue] } else { diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index ab1ad09559..6765dda05b 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -822,6 +822,7 @@ public final class PendingMessageManager { var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? + var videoTimestamp: Int32? var sendAsPeerId: PeerId? var quickReply: OutgoingQuickReplyMessageAttribute? var messageEffect: EffectMessageAttribute? @@ -859,6 +860,8 @@ public final class PendingMessageManager { messageEffect = attribute } else if let _ = attribute as? InvertMediaMessageAttribute { flags |= Int32(1 << 16) + } else if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp } } @@ -873,6 +876,9 @@ public final class PendingMessageManager { if hideCaptions { flags |= (1 << 12) } + if videoTimestamp != nil { + flags |= Int32(1 << 20) + } var sendAsInputPeer: Api.InputPeer? if let sendAsPeerId = sendAsPeerId, let sendAsPeer = transaction.getPeer(sendAsPeerId), let inputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: accountPeerId) { @@ -926,7 +932,7 @@ public final class PendingMessageManager { } else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) { let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id) - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), 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), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ForwardVideoTimestampAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ForwardVideoTimestampAttribute.swift new file mode 100644 index 0000000000..f23737d7b9 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ForwardVideoTimestampAttribute.swift @@ -0,0 +1,18 @@ +import Foundation +import Postbox + +public class ForwardVideoTimestampAttribute: MessageAttribute { + public let timestamp: Int32 + + public init(timestamp: Int32) { + self.timestamp = timestamp + } + + required public init(decoder: PostboxDecoder) { + self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.timestamp, forKey: "timestamp") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalMediaPlaybackInfoAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalMediaPlaybackInfoAttribute.swift new file mode 100644 index 0000000000..ecc83d4a59 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalMediaPlaybackInfoAttribute.swift @@ -0,0 +1,18 @@ +import Foundation +import Postbox + +public class LocalMediaPlaybackInfoAttribute: MessageAttribute { + public let data: Data + + public init(data: Data) { + self.data = data + } + + required public init(decoder: PostboxDecoder) { + self.data = decoder.decodeDataForKey("d") ?? Data() + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeData(self.data, forKey: "d") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 2ce6c8d17a..f19036ecf6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -140,6 +140,7 @@ public struct Namespaces { public static let cachedPremiumGiftCodeOptions: Int8 = 42 public static let cachedProfileGifts: Int8 = 43 public static let recommendedBots: Int8 = 44 + public static let channelsForPublicReaction: Int8 = 45 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index 94bc0cd63d..bb0bc2a6d4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -113,10 +113,10 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { break } } - var derivedData: DerivedDataMessageAttribute? + var previousDerivedData: DerivedDataMessageAttribute? for attribute in previous { if let attribute = attribute as? DerivedDataMessageAttribute { - derivedData = attribute + previousDerivedData = attribute break } } @@ -134,17 +134,16 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { updated.append(audioTranscription) } } - if let derivedData = derivedData { + if let previousDerivedData { var found = false for i in 0 ..< updated.count { - if let attribute = updated[i] as? DerivedDataMessageAttribute { - updated[i] = derivedData + if let _ = updated[i] as? DerivedDataMessageAttribute { found = true break } } if !found { - updated.append(derivedData) + updated.append(previousDerivedData) } } }, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 7a32432996..5e69125e09 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -468,7 +468,7 @@ private class AdMessagesHistoryContextImpl { } let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) } - let (contentMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let contentMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media parsedMessages.append(CachedMessage( opaqueId: randomId.makeData(), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift index f7e5d42833..473c45e194 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift @@ -14,7 +14,7 @@ func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to p flags |= (1 << 13) } - return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, scheduleDate: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) + return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, scheduleDate: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 3176bcd769..cfaf228f4f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1250,7 +1250,7 @@ func _internal_uploadStoryImpl( } id = idValue - let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId) + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, toPeerId).media if let parsedMedia = parsedMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile) } @@ -1593,7 +1593,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng if case let .updateStory(_, story) = update { switch story { case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _): - let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, account.peerId).media if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false, skipPreviews: updatingCoverTime) } @@ -2018,7 +2018,7 @@ extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { case let .storyItem(flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction): - let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? if let privacy = privacy { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 1973674532..83d0119ae9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -589,6 +589,28 @@ public extension TelegramEngine { |> ignoreValues } + public func updateLocallyDerivedData(messageId: MessageId, update: @escaping ([String: CodableEntry]) -> [String: CodableEntry]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + transaction.updateMessage(messageId, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var attributes = currentMessage.attributes + var data: [String: CodableEntry] = [:] + if let index = attributes.firstIndex(where: { $0 is DerivedDataMessageAttribute }) { + data = (attributes[index] as? DerivedDataMessageAttribute)?.data ?? [:] + attributes.remove(at: index) + } + data = update(data) + + if !data.isEmpty { + attributes.append(DerivedDataMessageAttribute(data: data)) + } + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + |> ignoreValues + } + public func rateAudioTranscription(messageId: MessageId, id: Int64, isGood: Bool) -> Signal { return _internal_rateAudioTranscription(postbox: self.account.postbox, network: self.account.network, messageId: messageId, id: id, isGood: isGood) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 7d54e95925..233b09824f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -583,21 +583,25 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann final class CachedStorySendAsPeers: Codable { public let peerIds: [PeerId] + public let timestamp: Double - public init(peerIds: [PeerId]) { + public init(peerIds: [PeerId], timestamp: Double) { self.peerIds = peerIds + self.timestamp = timestamp } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) self.peerIds = try container.decode([Int64].self, forKey: "l").map(PeerId.init) + self.timestamp = try container.decodeIfPresent(Double.self, forKey: "ts") ?? 0.0 } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l") + try container.encode(self.timestamp, forKey: "ts") } } @@ -644,7 +648,7 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { } } - if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id))) { + if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id), timestamp: CFAbsoluteTimeGetCurrent())) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)), entry: entry) } @@ -660,6 +664,77 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { } } +func _internal_channelsForPublicReaction(account: Account, useLocalCache: Bool) -> Signal<[Peer], NoError> { + let accountPeerId = account.peerId + return account.postbox.transaction { transaction -> ([Peer], Double)? in + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.channelsForPublicReaction, key: ValueBoxKey(length: 0)))?.get(CachedStorySendAsPeers.self) { + return (entry.peerIds.compactMap(transaction.getPeer), entry.timestamp) + } else { + return nil + } + } + |> mapToSignal { cachedPeers in + let remote: Signal<[Peer], NoError> = account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: 0)) + |> retryRequest + |> mapToSignal { result -> Signal<[Peer], NoError> in + return account.postbox.transaction { transaction -> [Peer] in + let chats: [Api.Chat] + let parsedPeers: AccumulatedPeers + switch result { + case let .chats(apiChats): + chats = apiChats + case let .chatsSlice(_, apiChats): + chats = apiChats + } + parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + var peers: [Peer] = [] + for chat in chats { + if let peer = transaction.getPeer(chat.peerId) { + peers.append(peer) + + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { + transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in + var current = current as? CachedChannelData ?? CachedChannelData() + var participantsSummary = current.participantsSummary + + participantsSummary.memberCount = participantsCount + + current = current.withUpdatedParticipantsSummary(participantsSummary) + return current + }) + } + } + } + + if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id), timestamp: CFAbsoluteTimeGetCurrent())) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.channelsForPublicReaction, key: ValueBoxKey(length: 0)), entry: entry) + } + + return peers + } + } + + if useLocalCache { + if let cachedPeers { + return .single(cachedPeers.0) + } else { + return .single([]) + } + } + + if let cachedPeers { + if CFAbsoluteTimeGetCurrent() < cachedPeers.1 + 5 * 60 { + return .single(cachedPeers.0) + } else { + return .single(cachedPeers.0) |> then(remote) + } + } else { + return remote + } + } +} + public enum ChannelAddressNameAssignmentAvailability { case available case unknown diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 986ec158e2..d48cfc1344 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -144,6 +144,13 @@ public extension TelegramEngine { return peers.map(EnginePeer.init) } } + + public func channelsForPublicReaction(useLocalCache: Bool) -> Signal<[EnginePeer], NoError> { + return _internal_channelsForPublicReaction(account: self.account, useLocalCache: useLocalCache) + |> map { peers -> [EnginePeer] in + return peers.map(EnginePeer.init) + } + } public func channelAddressNameAssignmentAvailability(peerId: PeerId?) -> Signal { return _internal_channelAddressNameAssignmentAvailability(account: self.account, peerId: peerId) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ConferenceButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ConferenceButtonView.swift new file mode 100644 index 0000000000..c04926106a --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ConferenceButtonView.swift @@ -0,0 +1,104 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import UIKitRuntimeUtils +import AppBundle + +final class ConferenceButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol { + private struct Params: Equatable { + var size: CGSize + + init(size: CGSize) { + self.size = size + } + } + + private let backdropBackgroundView: RoundedCornersView + private let iconView: UIImageView + + var pressAction: (() -> Void)? + + private var params: Params? + + let maskContents: UIView + override static var layerClass: AnyClass { + return MirroringLayer.self + } + + override init(frame: CGRect) { + self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true) + + self.iconView = UIImageView() + + self.maskContents = UIView() + self.maskContents.addSubview(self.backdropBackgroundView) + + super.init(frame: frame) + + self.addSubview(self.iconView) + + (self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer + + self.internalHighligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } else { + let t = self.layer.presentation()?.transform ?? layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + let transition = ComponentTransition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in + guard let self, completed else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.pressAction?() + } + + func update(size: CGSize, transition: ComponentTransition) { + let params = Params(size: size) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: ComponentTransition) { + self.backdropBackgroundView.update(cornerRadius: params.size.height * 0.5, transition: transition) + transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + + if self.iconView.image == nil { + self.iconView.image = UIImage(bundleImageName: "Contact List/AddMemberIcon")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = .white + } + + if let image = self.iconView.image { + let fraction: CGFloat = 1.0 + let imageSize = CGSize(width: floor(image.size.width * fraction), height: floor(image.size.height * fraction)) + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((params.size.width - imageSize.width) * 0.5), y: floorToScreenPixels((params.size.height - imageSize.height) * 0.5)), size: imageSize)) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 5205aba59e..edb759caee 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -80,6 +80,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool public var isEnergySavingEnabled: Bool + public var isConferencePossible: Bool public init( strings: PresentationStrings, @@ -93,7 +94,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu localVideo: VideoSource?, remoteVideo: VideoSource?, isRemoteBatteryLow: Bool, - isEnergySavingEnabled: Bool + isEnergySavingEnabled: Bool, + isConferencePossible: Bool ) { self.strings = strings self.lifecycleState = lifecycleState @@ -107,6 +109,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow self.isEnergySavingEnabled = isEnergySavingEnabled + self.isConferencePossible = isConferencePossible } public static func ==(lhs: State, rhs: State) -> Bool { @@ -146,6 +149,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if lhs.isEnergySavingEnabled != rhs.isEnergySavingEnabled { return false } + if lhs.isConferencePossible != rhs.isConferencePossible { + return false + } return true } } @@ -178,6 +184,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private let avatarLayer: AvatarLayer private let titleView: TextView private let backButtonView: BackButtonView + private var conferenceButtonView: ConferenceButtonView? private var statusView: StatusView private var weakSignalView: WeakSignalView? @@ -907,7 +914,53 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu transition.setFrame(view: self.backButtonView, frame: backButtonFrame) 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 @@ -923,13 +976,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu return } if !self.isEmojiKeyExpanded { - #if DEBUG - self.conferenceAddParticipant?() - #else self.isEmojiKeyExpanded = true self.displayEmojiTooltip = false self.update(transition: .spring(duration: 0.4)) - #endif } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD index 660c84d84c..31db545b01 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/Utils/RangeSet", + "//submodules/MediaResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index aeed9b708e..cf8a4d0a01 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -33,6 +33,7 @@ import WallpaperPreviewMedia import TextNodeWithEntities import RangeSet import GiftItemComponent +import MediaResources private struct FetchControls { let fetch: (Bool) -> Void @@ -444,6 +445,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public let dateAndStatusNode: ChatMessageDateAndStatusNode private var badgeNode: ChatMessageInteractiveMediaBadge? + private var timestampContainerView: UIView? + private var timestampMaskView: UIImageView? + private var videoTimestampBackgroundLayer: SimpleLayer? + private var videoTimestampForegroundLayer: SimpleLayer? + private var extendedMediaOverlayNode: ExtendedMediaOverlayNode? private var context: AccountContext? @@ -619,6 +625,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 { @@ -1935,6 +1948,95 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } + var videoTimestamp: Int32? + var storedVideoTimestamp: Int32? + for attribute in message.attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp + } else if let attribute = attribute as? DerivedDataMessageAttribute { + if let value = attribute.data["mps"]?.get(MediaPlaybackStoredState.self) { + storedVideoTimestamp = Int32(value.timestamp) + } + } + } + if let storedVideoTimestamp { + videoTimestamp = storedVideoTimestamp + } + + if let videoTimestamp, let file = media as? TelegramMediaFile, let duration = file.duration, duration > 1.0 { + let timestampContainerView: UIView + if let current = strongSelf.timestampContainerView { + timestampContainerView = current + } else { + timestampContainerView = UIView() + timestampContainerView.isUserInteractionEnabled = false + strongSelf.timestampContainerView = timestampContainerView + strongSelf.view.addSubview(timestampContainerView) + } + + let timestampMaskView: UIImageView + if let current = strongSelf.timestampMaskView { + timestampMaskView = current + } else { + timestampMaskView = UIImageView() + strongSelf.timestampMaskView = timestampMaskView + timestampContainerView.mask = timestampMaskView + + timestampMaskView.image = strongSelf.imageNode.image + } + + let videoTimestampBackgroundLayer: SimpleLayer + if let current = strongSelf.videoTimestampBackgroundLayer { + videoTimestampBackgroundLayer = current + } else { + videoTimestampBackgroundLayer = SimpleLayer() + strongSelf.videoTimestampBackgroundLayer = videoTimestampBackgroundLayer + timestampContainerView.layer.addSublayer(videoTimestampBackgroundLayer) + } + + let videoTimestampForegroundLayer: SimpleLayer + if let current = strongSelf.videoTimestampForegroundLayer { + videoTimestampForegroundLayer = current + } else { + videoTimestampForegroundLayer = SimpleLayer() + strongSelf.videoTimestampForegroundLayer = videoTimestampForegroundLayer + timestampContainerView.layer.addSublayer(videoTimestampForegroundLayer) + } + + videoTimestampBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor + videoTimestampForegroundLayer.backgroundColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.accentControlColor.cgColor : presentationData.theme.theme.chat.message.outgoing.accentControlColor.cgColor + + timestampContainerView.frame = imageFrame + timestampMaskView.frame = imageFrame + + let videoTimestampBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: imageFrame.height - 3.0), size: CGSize(width: imageFrame.width, height: 3.0)) + videoTimestampBackgroundLayer.frame = videoTimestampBackgroundFrame + + var fraction = Double(videoTimestamp) / duration + fraction = max(0.0, min(1.0, fraction)) + + let foregroundWidth = round(fraction * videoTimestampBackgroundFrame.width) + let videoTimestampForegroundFrame = CGRect(origin: CGPoint(x: videoTimestampBackgroundFrame.minX, y: videoTimestampBackgroundFrame.minY), size: CGSize(width: foregroundWidth, height: videoTimestampBackgroundFrame.height)) + videoTimestampForegroundLayer.frame = videoTimestampForegroundFrame + } else { + if let timestampContainerView = strongSelf.timestampContainerView { + strongSelf.timestampContainerView = nil + timestampContainerView.removeFromSuperview() + } + if let timestampMaskView = strongSelf.timestampMaskView { + strongSelf.timestampMaskView = nil + timestampMaskView.removeFromSuperview() + } + if let videoTimestampBackgroundLayer = strongSelf.videoTimestampBackgroundLayer { + strongSelf.videoTimestampBackgroundLayer = nil + videoTimestampBackgroundLayer.removeFromSuperlayer() + } + if let videoTimestampForegroundLayer = strongSelf.videoTimestampForegroundLayer { + strongSelf.videoTimestampForegroundLayer = nil + videoTimestampForegroundLayer.removeFromSuperlayer() + } + } + if let animatedStickerNode = strongSelf.animatedStickerNode { animatedStickerNode.frame = imageFrame animatedStickerNode.updateLayout(size: imageFrame.size) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index aa3ec3194d..99c73109a9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -21,6 +21,7 @@ import BundleIconComponent import CheckNode import TextFormat import CheckComponent +import ContextUI private final class BalanceComponent: CombinedComponent { let context: AccountContext @@ -821,6 +822,8 @@ private final class ChatSendStarsScreenComponent: Component { let context: AccountContext let peer: EnginePeer let myPeer: EnginePeer + let sendAsPeer: EnginePeer + let channelsForPublicReaction: [EnginePeer] let messageId: EngineMessage.Id let maxAmount: Int let balance: StarsAmount? @@ -833,6 +836,8 @@ private final class ChatSendStarsScreenComponent: Component { context: AccountContext, peer: EnginePeer, myPeer: EnginePeer, + sendAsPeer: EnginePeer, + channelsForPublicReaction: [EnginePeer], messageId: EngineMessage.Id, maxAmount: Int, balance: StarsAmount?, @@ -844,6 +849,8 @@ private final class ChatSendStarsScreenComponent: Component { self.context = context self.peer = peer self.myPeer = myPeer + self.sendAsPeer = sendAsPeer + self.channelsForPublicReaction = channelsForPublicReaction self.messageId = messageId self.maxAmount = maxAmount self.balance = balance @@ -863,6 +870,12 @@ private final class ChatSendStarsScreenComponent: Component { if lhs.myPeer != rhs.myPeer { return false } + if lhs.sendAsPeer != rhs.sendAsPeer { + return false + } + if lhs.channelsForPublicReaction != rhs.channelsForPublicReaction { + return false + } if lhs.maxAmount != rhs.maxAmount { return false } @@ -988,6 +1001,7 @@ private final class ChatSendStarsScreenComponent: Component { private let hierarchyTrackingNode: HierarchyTrackingNode private let leftButton = ComponentView() + private let peerSelectorButton = ComponentView() private let closeButton = ComponentView() private let title = ComponentView() @@ -1037,6 +1051,10 @@ private final class ChatSendStarsScreenComponent: Component { private var badgePhysicsLink: SharedDisplayLinkDriver.Link? + private var currentMyPeer: EnginePeer? + private var channelsForPublicReaction: [EnginePeer] = [] + private var channelsForPublicReactionDisposable: Disposable? + override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -1119,6 +1137,7 @@ private final class ChatSendStarsScreenComponent: Component { deinit { self.balanceDisposable?.dispose() + self.channelsForPublicReactionDisposable?.dispose() } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -1297,6 +1316,87 @@ private final class ChatSendStarsScreenComponent: Component { } } + private func displayTargetSelectionMenu(sourceView: UIView) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + + var items: [ContextMenuItem] = [] + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + var peers: [EnginePeer] = [component.myPeer] + peers.append(contentsOf: self.channelsForPublicReaction) + + let avatarSize = CGSize(width: 30.0, height: 30.0) + + for peer in peers { + let peerLabel: String + if peer.id == component.context.account.peerId { + peerLabel = environment.strings.AffiliateProgram_PeerTypeSelf + } else if case .channel = peer { + peerLabel = environment.strings.Channel_Status + } else { + peerLabel = environment.strings.Bot_GenericBotStatus + } + let isSelected = peer.id == self.currentMyPeer?.id + let accentColor = environment.theme.list.itemAccentColor + let avatarSignal = peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize) + |> map { image in + let context = DrawingContext(size: avatarSize, scale: 0.0, clear: true) + context?.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + if isSelected { + + } + c.saveGState() + let scaleFactor = (avatarSize.width - 3.0 * 2.0) / avatarSize.width + if isSelected { + c.translateBy(x: avatarSize.width * 0.5, y: avatarSize.height * 0.5) + c.scaleBy(x: scaleFactor, y: scaleFactor) + c.translateBy(x: -avatarSize.width * 0.5, y: -avatarSize.height * 0.5) + } + if let image { + image.draw(in: CGRect(origin: CGPoint(), size: avatarSize)) + } + c.restoreGState() + + if isSelected { + c.setStrokeColor(accentColor.cgColor) + let lineWidth: CGFloat = 1.0 + UIScreenPixel + c.setLineWidth(lineWidth) + c.strokeEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + } + } + return context?.generateImage() + } + items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { [weak self] c, _ in + c?.dismiss(completion: {}) + + guard let self, let component = self.component else { + return + } + if self.currentMyPeer?.id == peer.id { + return + } + + if self.currentMyPeer != peer { + self.currentMyPeer = peer + + let _ = component.context.engine.peers.updatePeerSendAsPeer(peerId: component.peer.id, sendAs: peer.id).startStandalone() + } + + self.state?.updated(transition: .immediate) + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -1312,6 +1412,8 @@ private final class ChatSendStarsScreenComponent: Component { let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 16.0 if self.component == nil { + self.currentMyPeer = component.myPeer + self.balance = component.balance var isLogarithmic = true if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_stars_reaction_logarithmic_scale"] as? Double { @@ -1336,6 +1438,17 @@ private final class ChatSendStarsScreenComponent: Component { } }) } + + self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false) + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + guard let self else { + return + } + if self.channelsForPublicReaction != peers { + self.channelsForPublicReaction = peers + self.state?.updated(transition: .immediate) + } + }) } self.component = component @@ -1514,6 +1627,9 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 123.0 + var sendAsPeers: [EnginePeer] = [component.myPeer] + sendAsPeers.append(contentsOf: self.channelsForPublicReaction) + let leftButtonSize = self.leftButton.update( transition: transition, component: AnyComponent(BalanceComponent( @@ -1531,6 +1647,35 @@ private final class ChatSendStarsScreenComponent: Component { self.navigationBarContainer.addSubview(leftButtonView) } transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + leftButtonView.isHidden = sendAsPeers.count > 1 + } + + let currentMyPeer = self.currentMyPeer ?? component.myPeer + + let peerSelectorButtonSize = self.peerSelectorButton.update( + transition: transition, + component: AnyComponent(PeerSelectorBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: currentMyPeer, + action: { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((56.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) + if let peerSelectorButtonView = self.peerSelectorButton.view { + if peerSelectorButtonView.superview == nil { + self.navigationBarContainer.addSubview(peerSelectorButtonView) + } + transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) + peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } if themeUpdated { @@ -1717,7 +1862,7 @@ private final class ChatSendStarsScreenComponent: Component { if myCount != 0 { mappedTopPeers.append(ChatSendStarsScreen.TopPeer( randomIndex: -1, - peer: self.isAnonymous ? nil : component.myPeer, + peer: self.isAnonymous ? nil : currentMyPeer, isMy: true, count: myCount )) @@ -2102,6 +2247,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { public final class InitialData { fileprivate let peer: EnginePeer fileprivate let myPeer: EnginePeer + fileprivate let sendAsPeer: EnginePeer + fileprivate let channelsForPublicReaction: [EnginePeer] fileprivate let messageId: EngineMessage.Id fileprivate let balance: StarsAmount? fileprivate let currentSentAmount: Int? @@ -2111,6 +2258,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { fileprivate init( peer: EnginePeer, myPeer: EnginePeer, + sendAsPeer: EnginePeer, + channelsForPublicReaction: [EnginePeer], messageId: EngineMessage.Id, balance: StarsAmount?, currentSentAmount: Int?, @@ -2119,6 +2268,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { ) { self.peer = peer self.myPeer = myPeer + self.sendAsPeer = sendAsPeer + self.channelsForPublicReaction = channelsForPublicReaction self.messageId = messageId self.balance = balance self.currentSentAmount = currentSentAmount @@ -2204,6 +2355,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { context: context, peer: initialData.peer, myPeer: initialData.myPeer, + sendAsPeer: initialData.sendAsPeer, + channelsForPublicReaction: initialData.channelsForPublicReaction, messageId: initialData.messageId, maxAmount: maxAmount, balance: initialData.balance, @@ -2266,15 +2419,20 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { topPeers = Array(topPeers.prefix(3)) } + let channelsForPublicReaction = context.engine.peers.channelsForPublicReaction(useLocalCache: true) + let sendAsPeer: Signal = .single(nil) + return combineLatest( context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), EngineDataMap(allPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ), - balance + balance, + sendAsPeer, + channelsForPublicReaction ) - |> map { peerAndTopPeerMap, balance -> InitialData? in + |> map { peerAndTopPeerMap, balance, sendAsPeer, channelsForPublicReaction -> InitialData? in let (peer, myPeer, topPeerMap) = peerAndTopPeerMap guard let peer, let myPeer else { return nil @@ -2284,6 +2442,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { return InitialData( peer: peer, myPeer: myPeer, + sendAsPeer: sendAsPeer ?? myPeer, + channelsForPublicReaction: channelsForPublicReaction, messageId: messageId, balance: balance, currentSentAmount: currentSentAmount, @@ -2570,3 +2730,172 @@ private final class SliderStarsView: UIView { self.emitterLayer.emitterSize = size } } + +private final class PeerSelectorBadgeComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + let action: ((UIView) -> Void)? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer, + action: ((UIView) -> Void)? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.action = action + } + + static func ==(lhs: PeerSelectorBadgeComponent, rhs: PeerSelectorBadgeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: HighlightableButton { + private let background = ComponentView() + private var avatarNode: AvatarNode? + private var selectorIcon: ComponentView? + + private var component: PeerSelectorBadgeComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action?(self) + } + + func update(component: PeerSelectorBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.isEnabled = component.action != nil + + let height: CGFloat = 32.0 + let avatarPadding: CGFloat = 1.0 + + let avatarDiameter = height - avatarPadding * 2.0 + let avatarTextSpacing: CGFloat = -4.0 + let rightTextInset: CGFloat = component.action != nil ? 26.0 : 12.0 + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(avatarDiameter * 0.5))) + avatarNode.isUserInteractionEnabled = false + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + avatarNode.frame = avatarFrame + avatarNode.updateSize(size: avatarFrame.size) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, synchronousLoad: true) + + let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + rightTextInset, height: height) + + if component.action != nil { + let selectorIcon: ComponentView + if let current = self.selectorIcon { + selectorIcon = current + } else { + selectorIcon = ComponentView() + self.selectorIcon = selectorIcon + } + let selectorIconSize = selectorIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Item List/ExpandableSelectorArrows", tintColor: component.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5))), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize) + if let selectorIconView = selectorIcon.view { + if selectorIconView.superview == nil { + selectorIconView.isUserInteractionEnabled = false + self.addSubview(selectorIconView) + } + transition.setFrame(view: selectorIconView, frame: selectorIconFrame) + } + } else if let selectorIcon = self.selectorIcon { + self.selectorIcon = nil + selectorIcon.view?.removeFromSuperview() + } + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.theme.list.itemInputField.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + private let actionsOnTop: Bool + + init(controller: ViewController, sourceView: UIView, actionsOnTop: Bool) { + self.controller = controller + self.sourceView = sourceView + self.actionsOnTop = actionsOnTop + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 3df171977a..f9cc7f1364 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -100,6 +100,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { private let title = ComponentView() private let subtitle = ComponentView() + private let openBotButton = ComponentView() private var dailyRevenueText: ComponentView? private let titleTransformContainer: UIView private let bottomPanelContainer: UIView @@ -832,6 +833,84 @@ 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))) + //TODO:localize + var openBotComponents: [AnyComponentWithIdentity] = [] + var openBotLeftInset: CGFloat = 12.0 + if case .active = component.mode { + openBotLeftInset = 1.0 + openBotComponents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(AvatarComponent( + context: component.context, + peer: component.sourcePeer, + size: CGSize(width: 30.0, height: 30.0) + )), fixedSize: CGSize(width: 30.0, height: 2.0), + translation: CGPoint(x: 0.0, y: 1.0))))) + } + openBotComponents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "View " + component.sourcePeer.compactDisplayTitle, font: Font.medium(15.0), textColor: environment.theme.list.itemInputField.primaryColor)) + )))) + openBotComponents.append(AnyComponentWithIdentity(id: 2, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: environment.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5), + scaleFactor: 0.8 + )), + fixedSize: CGSize(width: 8.0, height: 2.0), + translation: CGPoint(x: 0.0, y: 2.0) + )))) + let openBotButtonSize = self.openBotButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack(openBotComponents, spacing: 2.0)), + background: AnyComponent(FilledRoundedRectangleComponent(color: environment.theme.list.itemInputField.backgroundColor, cornerRadius: .minEdge, smoothCorners: false)), + effectAlignment: .center, + minSize: CGSize(width: 1.0, height: 30.0 + 2.0), + contentInsets: UIEdgeInsets(top: 0.0, left: openBotLeftInset, bottom: 0.0, right: 12.0), + action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + guard let infoController = component.context.sharedContext.makePeerInfoController( + context: component.context, + updatedPresentationData: nil, + peer: component.sourcePeer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) else { + return + } + controller.dismiss(completion: { [weak navigationController] in + DispatchQueue.main.async { + guard let navigationController else { + return + } + navigationController.pushViewController(infoController) + } + }) + }, + animateAlpha: true, + animateScale: true, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let openBotButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - openBotButtonSize.width) * 0.5), y: contentHeight), size: openBotButtonSize) + if let openBotButtonView = self.openBotButton.view { + if openBotButtonView.superview == nil { + self.scrollContentView.addSubview(openBotButtonView) + } + transition.setPosition(view: openBotButtonView, position: openBotButtonFrame.center) + openBotButtonView.bounds = CGRect(origin: CGPoint(), size: openBotButtonFrame.size) + } + contentHeight += openBotButtonSize.height + contentHeight += 20.0 + let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 0336afd47c..1dad042ac5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -6266,7 +6266,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil) - let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact)), updatedPresentationData: strongSelf.controller?.updatedPresentationData) + let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact), nil), updatedPresentationData: strongSelf.controller?.updatedPresentationData) shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 0273a7f3ac..d7c31a35d2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2599,7 +2599,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let shareController = ShareController( context: self.context, - subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false))), + subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false)), nil), presetText: nil, preferredAction: .default, showInChat: nil, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 5bcd1fc15b..5bacb94618 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1035,7 +1035,7 @@ final class StoryItemSetContainerSendMessage { let shareController = ShareController( context: component.context, - subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false))), + subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false)), nil), preferredAction: preferredAction ?? .default, externalShare: false, immediateExternalShare: false, diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/Contents.json new file mode 100644 index 0000000000..acc6641f90 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrows.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/arrows.pdf b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/arrows.pdf new file mode 100644 index 0000000000..31e7762d53 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandableSelectorArrows.imageset/arrows.pdf @@ -0,0 +1,88 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.834991 2.770020 cm +0.000000 0.000000 0.000000 scn +4.135217 12.200247 m +4.010505 12.324958 3.841359 12.395020 3.664990 12.395020 c +3.488621 12.395020 3.319476 12.324956 3.194765 12.200244 c +0.194780 9.200245 l +-0.064918 8.940545 -0.064917 8.519490 0.194782 8.259792 c +0.454482 8.000094 0.875536 8.000095 1.135234 8.259794 c +3.664994 10.789568 l +6.194782 8.259792 l +6.454482 8.000094 6.875536 8.000095 7.135234 8.259794 c +7.394932 8.519494 7.394931 8.940549 7.135232 9.200247 c +4.135217 12.200247 l +h +7.135226 3.259784 m +4.135226 0.259784 l +3.875527 0.000086 3.454473 0.000086 3.194774 0.259784 c +0.194774 3.259784 l +-0.064925 3.519483 -0.064925 3.940537 0.194774 4.200236 c +0.454473 4.459935 0.875527 4.459935 1.135226 4.200236 c +3.665000 1.670462 l +6.194774 4.200236 l +6.454473 4.459935 6.875527 4.459935 7.135226 4.200236 c +7.394925 3.940537 7.394925 3.519483 7.135226 3.259784 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 959 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 9.000000 18.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001049 00000 n +0000001071 00000 n +0000001243 00000 n +0000001317 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1376 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index a598a5a4a1..c28cea930f 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -305,7 +305,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } override func loadDisplayNode() { - self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, attemptDisabledItemSelection: self.attemptDisabledItemSelection, options: self.options, filters: self.filters, onlyWriteable: self.onlyWriteable, isGroupInvitation: self.isGroupInvitation, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit, present: { [weak self] c, a in + self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, updatedPresentationData: self.params.updatedPresentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, attemptDisabledItemSelection: self.attemptDisabledItemSelection, options: self.options, filters: self.filters, onlyWriteable: self.onlyWriteable, isGroupInvitation: self.isGroupInvitation, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }) switch self.contactsNode.contentNode { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index b136def091..efb93aee9b 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -82,7 +82,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let onlyWriteable: Bool private let isGroupInvitation: Bool - init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { + init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { self.navigationBar = navigationBar self.context = context @@ -241,7 +241,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { return .natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers) } - let contactListNode = ContactListNode(context: context, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState()) + let contactListNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState()) self.contentNode = .contacts(contactListNode) if !selectedPeers.isEmpty { @@ -365,7 +365,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { case .premiumGifting, .requestedUsersSelection: searchChatList = true } - let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(ContactListPresentation.Search( + let searchResultsNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: .single(.search(ContactListPresentation.Search( signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, @@ -373,7 +373,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { searchChannels: searchChannels, globalSearch: globalSearch, displaySavedMessages: displaySavedMessages - ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) + ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) searchResultsNode.openPeer = { peer, _, _, _ in self?.tokenListNode.setText("") self?.openPeer?(peer) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 86aa4c4867..123073f5d6 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -227,7 +227,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } if immediateShare { - let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true) + let controller = ShareController(context: params.context, subject: .media(.standalone(media: file), nil), immediateExternalShare: true) params.present(controller, nil, .window(.root)) } else if let rootController = params.navigationController?.view.window?.rootViewController { let proceed = { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index af350130a9..2d0a24f0bb 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1020,7 +1020,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } let statusBarContent: CallStatusBarNodeImpl.Content? - if let call { + if let call, !hasGroupCallOnScreen { statusBarContent = .call(strongSelf, call.context.account, call) } else if let groupCall = groupCall, !hasGroupCallOnScreen { statusBarContent = .groupCall(strongSelf, groupCall.account, groupCall) @@ -1055,7 +1055,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { if callController.isNodeLoaded { mainWindow.hostView.containerView.endEditing(true) if callController.view.superview == nil { - mainWindow.present(callController, on: .calls) + if useFlatModalCallsPresentation(context: callController.call.context) { + (mainWindow.viewController as? NavigationController)?.pushViewController(callController) + } else { + mainWindow.present(callController, on: .calls) + } } else { callController.expandFromPipIfPossible() } @@ -1276,11 +1280,30 @@ public final class SharedAccountContextImpl: SharedAccountContext { return } if callController.window == nil { - self.mainWindow?.present(callController, on: .calls) + if useFlatModalCallsPresentation(context: callController.call.context) { + (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) + } else { + self.mainWindow?.present(callController, on: .calls) + } } completion(true) } - self.mainWindow?.present(callController, on: .calls) + callController.onViewDidAppear = { [weak self] in + if let self { + self.hasGroupCallOnScreenPromise.set(true) + } + } + callController.onViewDidDisappear = { [weak self] in + if let self { + self.hasGroupCallOnScreenPromise.set(false) + } + } + if useFlatModalCallsPresentation(context: callController.call.context) { + self.hasGroupCallOnScreenPromise.set(true) + (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) + } else { + self.mainWindow?.present(callController, on: .calls) + } } } @@ -1513,7 +1536,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { if let callController = self.callController { if callController.isNodeLoaded && callController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) - mainWindow.present(callController, on: .calls) + + if useFlatModalCallsPresentation(context: callController.call.context) { + (mainWindow.viewController as? NavigationController)?.pushViewController(callController) + } else { + mainWindow.present(callController, on: .calls) + } } } else if let groupCallController = self.groupCallController { if groupCallController.isNodeLoaded && groupCallController.view.superview == nil { @@ -3211,3 +3239,10 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation } return nil } + +private func useFlatModalCallsPresentation(context: AccountContext) -> Bool { + if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_modalcalls"] != nil { + return false + } + return true +} diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index c894a82f00..ec477ff08c 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -86,7 +86,6 @@ public struct ApplicationSpecificItemCacheCollectionId { public static let instantPageStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.instantPageStoredState.rawValue) public static let cachedInstantPages = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedInstantPages.rawValue) public static let cachedWallpapers = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedWallpapers.rawValue) - public static let mediaPlaybackStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaPlaybackStoredState.rawValue) public static let cachedGeocodes = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedGeocodes.rawValue) public static let visualMediaStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.visualMediaStoredState.rawValue) public static let cachedImageRecognizedContent = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedImageRecognizedContent.rawValue)