This commit is contained in:
Isaac 2025-01-21 21:08:44 +04:00
parent 8a0643eb53
commit 40828e9037
58 changed files with 1473 additions and 220 deletions

View File

@ -49,12 +49,20 @@ public enum ShareControllerError {
}
public enum ShareControllerSubject {
public final class MediaParameters {
public let startAtTimestamp: Int32?
public init(startAtTimestamp: Int32?) {
self.startAtTimestamp = startAtTimestamp
}
}
case url(String)
case text(String)
case quote(text: String, url: String)
case messages([Message])
case image([ImageRepresentationWithReference])
case media(AnyMediaReference)
case media(AnyMediaReference, MediaParameters?)
case mapMedia(TelegramMediaMap)
case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal<ShareControllerExternalStatus, ShareControllerError>)
}

View File

@ -569,10 +569,10 @@ public class BrowserScreen: ViewController, MinimizableController {
var isDocument = false
if let content = self.content.last {
if let documentContent = content as? BrowserDocumentContent {
subject = .media(documentContent.file.abstract)
subject = .media(documentContent.file.abstract, nil)
isDocument = true
} else if let documentContent = content as? BrowserPdfContent {
subject = .media(documentContent.file.abstract)
subject = .media(documentContent.file.abstract, nil)
isDocument = true
} else {
subject = .url(url)

View File

@ -759,7 +759,7 @@ open class NavigationController: UINavigationController, ContainableController,
}
let effectiveModalTransition: CGFloat
if visibleModalCount == 0 || navigationLayout.modal[i].isFlat {
if visibleModalCount == 0 || (navigationLayout.modal[i].isFlat && !navigationLayout.modal[i].flatReceivesModalTransition) {
effectiveModalTransition = 0.0
} else if visibleModalCount == 1 {
effectiveModalTransition = 1.0 - topModalDismissProgress

View File

@ -11,6 +11,7 @@ enum RootNavigationLayout {
struct ModalContainerLayout {
var controllers: [ViewController]
var isFlat: Bool
var flatReceivesModalTransition: Bool
var isStandalone: Bool
}
@ -26,6 +27,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
let requiresModal: Bool
var beginsModal: Bool = false
var isFlat: Bool = false
var flatReceivesModalTransition: Bool = false
var isStandalone: Bool = false
switch controller.navigationPresentation {
case .default:
@ -39,6 +41,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
requiresModal = true
beginsModal = true
isFlat = true
flatReceivesModalTransition = controller.flatReceivesModalTransition
case .standaloneModal:
requiresModal = true
beginsModal = true
@ -68,7 +71,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
if requiresModal {
controller._presentedInModal = true
if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}
@ -78,7 +81,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
controller._presentedInModal = true
}
if modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}

View File

@ -342,9 +342,16 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
self.validLayout = layout
var isStandaloneModal = false
if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation {
isStandaloneModal = true
var flatReceivesModalTransition = false
if let controller = controllers.first {
if case .standaloneModal = controller.navigationPresentation {
isStandaloneModal = true
}
if controller.flatReceivesModalTransition {
flatReceivesModalTransition = true
}
}
let _ = flatReceivesModalTransition
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
self.ignoreScrolling = true
@ -378,7 +385,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
}
if isStandaloneModal || isLandscape || self.isFlat {
if isStandaloneModal || isLandscape || (self.isFlat && !flatReceivesModalTransition) {
self.container.cornerRadius = 0.0
} else {
self.container.cornerRadius = 10.0
@ -419,13 +426,19 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
let maxScaledTopInset: CGFloat = topInset - 10.0
var maxScaledTopInset: CGFloat = topInset - 10.0
if flatReceivesModalTransition {
maxScaledTopInset = 0.0
if let statusBarHeight = layout.statusBarHeight {
maxScaledTopInset += statusBarHeight
}
}
let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
}
} else {
self.panRecognizer?.isEnabled = false
if self.isFlat {
if self.isFlat && !flatReceivesModalTransition {
self.dim.backgroundColor = .clear
self.container.clipsToBounds = true
self.container.cornerRadius = 0.0

View File

@ -163,6 +163,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
open var navigationPresentation: ViewControllerNavigationPresentation = .default
open var _presentedInModal: Bool = false
open var flatReceivesModalTransition: Bool = false
public var presentedOverCoveringView: Bool = false

View File

@ -186,6 +186,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
var interacting: ((Bool) -> Void)?
var shareMediaParameters: (() -> ShareControllerSubject.MediaParameters?)?
private var seekTimer: SwiftSignalKit.Timer?
private var currentIsPaused: Bool = true
private var seekRate: Double = 1.0
@ -1644,16 +1646,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
}
} else {
if let file = content.file {
subject = .media(.webPage(webPage: WebpageReference(webpage), media: file))
subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), nil)
preferredAction = .saveToCameraRoll
} else if let image = content.image {
subject = .media(.webPage(webPage: WebpageReference(webpage), media: image))
subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), nil)
preferredAction = .saveToCameraRoll
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
}
}
} else if let file = m as? TelegramMediaFile {
subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file))
subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file), strongSelf.shareMediaParameters?())
if file.isAnimated {
if messages[0].id.peerId.namespace == Namespaces.Peer.SecretChat {
preferredAction = .default
@ -1666,7 +1668,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controllerInteraction = strongSelf.controllerInteraction
let _ = (toggleGifSaved(account: context.account, fileReference: .message(message: MessageReference(message._asMessage()), media: file), saved: true)
|> deliverOnMainQueue).start(next: { result in
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
@ -1873,7 +1875,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
}
var preferredAction = ShareControllerPreferredAction.default
var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media))
var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media), self.shareMediaParameters?())
if let file = media as? TelegramMediaFile {
if file.isAnimated {
@ -1935,10 +1937,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
}
} else {
if let file = content.file {
subject = .media(.webPage(webPage: WebpageReference(webpage), media: file))
subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), self.shareMediaParameters?())
preferredAction = .saveToCameraRoll
} else if let image = content.image {
subject = .media(.webPage(webPage: WebpageReference(webpage), media: image))
subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), self.shareMediaParameters?())
preferredAction = .saveToCameraRoll
}
}

View File

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

View File

@ -147,7 +147,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode {
@objc func actionButtonPressed() {
if let shareMedia = self.shareMedia {
self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil)
self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia, nil), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil)
}
}
}

View File

@ -3,6 +3,7 @@ import UIKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import Postbox
public final class MediaPlaybackStoredState: Codable {
public let timestamp: Double
@ -29,28 +30,28 @@ public final class MediaPlaybackStoredState: Codable {
}
public func mediaPlaybackStoredState(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal<MediaPlaybackStoredState?, NoError> {
let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
key.setInt32(16, value: messageId.id)
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key))
|> map { entry -> MediaPlaybackStoredState? in
return entry?.get(MediaPlaybackStoredState.self)
return engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> map { message -> MediaPlaybackStoredState? in
guard let message else {
return nil
}
for attribute in message.attributes {
if let attribute = attribute as? DerivedDataMessageAttribute {
return attribute.data["mps"]?.get(MediaPlaybackStoredState.self)
}
}
return nil
}
}
public func updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine, messageId: EngineMessage.Id, state: MediaPlaybackStoredState?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
key.setInt32(16, value: messageId.id)
if let state = state {
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key, item: state)
} else {
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key)
}
return engine.messages.updateLocallyDerivedData(messageId: messageId, update: { data in
var data = data
if let state, let entry = CodableEntry(state) {
data["mps"] = entry
} else {
data.removeValue(forKey: "mps")
}
return data
})
}

View File

@ -188,7 +188,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
var actionCompletionText: String?
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
subject = .media(videoFileReference.abstract)
subject = .media(videoFileReference.abstract, nil)
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
} else {
subject = .image(entry.representations)

View File

@ -521,7 +521,7 @@ public final class ThemePreviewController: ViewController {
subject = .url("https://t.me/addtheme/\(slug)")
preferredAction = .default
case let .media(media):
subject = .media(media)
subject = .media(media, nil)
preferredAction = .default
}
let controller = ShareController(context: self.context, subject: subject, preferredAction: preferredAction)

View File

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

View File

@ -3,6 +3,7 @@ import UIKit
import AsyncDisplayKit
import Display
import ContextUI
import CheckNode
public final class ShareActionButtonNode: HighlightTrackingButtonNode {
private let referenceNode: ContextReferenceContentNode
@ -109,3 +110,74 @@ public final class ShareActionButtonNode: HighlightTrackingButtonNode {
self.referenceNode.frame = self.bounds
}
}
public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode {
private let checkNode: CheckNode
private let titleTextNode: TextNode
public var titleTextColor: UIColor {
didSet {
self.setNeedsLayout()
}
}
public var checkNodeTheme: CheckNodeTheme {
didSet {
self.checkNode.theme = self.checkNodeTheme
}
}
private let titleText: String
public var value: Bool {
return self.checkNode.selected
}
public init(titleText: String, titleTextColor: UIColor, checkNodeTheme: CheckNodeTheme) {
self.titleText = titleText
self.titleTextColor = titleTextColor
self.checkNodeTheme = checkNodeTheme
self.checkNode = CheckNode(theme: checkNodeTheme, content: .check)
self.checkNode.isUserInteractionEnabled = false
self.titleTextNode = TextNode()
self.titleTextNode.isUserInteractionEnabled = false
self.titleTextNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.checkNode)
self.addSubnode(self.titleTextNode)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.checkNode.setSelected(!self.checkNode.selected, animated: true)
}
override public func layout() {
super.layout()
if self.bounds.width < 1.0 {
return
}
let checkSize: CGFloat = 18.0
let checkSpacing: CGFloat = 10.0
let (titleTextLayout, titleTextApply) = TextNode.asyncLayout(self.titleTextNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.titleText, font: Font.regular(13.0), textColor: self.titleTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: self.bounds.width - 8.0 * 2.0 - checkSpacing - checkSize, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let _ = titleTextApply()
let contentWidth = checkSize + checkSpacing + titleTextLayout.size.width
let checkFrame = CGRect(origin: CGPoint(x: floor((self.bounds.width - contentWidth) * 0.5), y: floor((self.bounds.height - checkSize) * 0.5)), size: CGSize(width: checkSize, height: checkSize))
let isFirstTime = self.checkNode.bounds.isEmpty
self.checkNode.frame = checkFrame
if isFirstTime {
self.checkNode.setSelected(false, animated: false)
}
self.titleTextNode.frame = CGRect(origin: CGPoint(x: checkFrame.maxX + checkSpacing, y: floor((self.bounds.height - titleTextLayout.size.height) * 0.5)), size: titleTextLayout.size)
}
}

View File

@ -524,7 +524,7 @@ public final class ShareController: ViewController {
self?.actionCompleted?()
})
}
case let .media(mediaReference):
case let .media(mediaReference, _):
var canSave = false
var isVideo = false
if mediaReference.media is TelegramMediaImage {
@ -668,7 +668,12 @@ public final class ShareController: ViewController {
fromPublicChannel = true
}
self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in
var mediaParameters: ShareControllerSubject.MediaParameters?
if case let .media(_, parameters) = self.subject {
mediaParameters = parameters
}
self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, mediaParameters: mediaParameters, requestLayout: { [weak self] transition in
self?.requestLayout(transition: transition)
}, presentError: { [weak self] title, text in
guard let strongSelf = self else {
@ -765,7 +770,7 @@ public final class ShareController: ViewController {
return false
}
}
case let .media(mediaReference):
case let .media(mediaReference, _):
var sendTextAsCaption = false
if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile {
sendTextAsCaption = true
@ -981,7 +986,7 @@ public final class ShareController: ViewController {
case let .image(representations):
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: .standalone(media: media)))
case let .media(mediaReference):
case let .media(mediaReference, _):
collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: mediaReference))
case let .mapMedia(media):
let latLong = "\(media.latitude),\(media.longitude)"
@ -1518,7 +1523,7 @@ public final class ShareController: ViewController {
messages: messages
))
}
case let .media(mediaReference):
case let .media(mediaReference, _):
var sendTextAsCaption = false
if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile {
sendTextAsCaption = true
@ -2041,7 +2046,7 @@ public final class ShareController: ViewController {
messages = transformMessages(messages, showNames: showNames, silently: silently)
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .media(mediaReference):
case let .media(mediaReference, mediaParameters):
var sendTextAsCaption = false
if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile {
sendTextAsCaption = true
@ -2116,7 +2121,15 @@ public final class ShareController: ViewController {
if !text.isEmpty && !sendTextAsCaption {
messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
var attributes: [MessageAttribute] = []
if let startAtTimestamp = mediaParameters?.startAtTimestamp, let startAtTimestampNode = strongSelf.controllerNode.startAtTimestampNode, startAtTimestampNode.value {
attributes.append(ForwardVideoTimestampAttribute(timestamp: startAtTimestamp))
}
if case let .message(message, _) = mediaReference, let sourceMessageId = message.id, (sourceMessageId.peerId.namespace == Namespaces.Peer.CloudUser || sourceMessageId.peerId.namespace == Namespaces.Peer.CloudGroup || sourceMessageId.peerId.namespace == Namespaces.Peer.CloudChannel) {
messages.append(.forward(source: sourceMessageId, threadId: threadId, grouping: .auto, attributes: attributes, correlationId: nil))
} else {
messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages = transformMessages(messages, showNames: showNames, silently: silently)
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}

View File

@ -14,6 +14,7 @@ import MultilineTextComponent
import TelegramStringFormatting
import BundleIconComponent
import LottieComponent
import CheckNode
enum ShareState {
case preparing(Bool)
@ -296,6 +297,22 @@ private final class ShareContentInfoView: UIView {
}
}
private func textForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
let secondsPadding = seconds < 10 ? "0" : ""
return "\(minutes):\(secondsPadding)\(seconds)"
} else {
let hours = value / 3600
let minutes = (value % 3600) / 60
let minutesPadding = minutes < 10 ? "0" : ""
let seconds = value % 60
let secondsPadding = seconds < 10 ? "0" : ""
return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
}
}
final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate {
private weak var controller: ShareController?
private let environment: ShareControllerEnvironment
@ -332,6 +349,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
private let actionsBackgroundNode: ASImageNode
private let actionButtonNode: ShareActionButtonNode
let startAtTimestampNode: ShareStartAtTimestampNode?
private let inputFieldNode: ShareInputFieldNode
private let actionSeparatorNode: ASDisplayNode
@ -366,7 +384,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
private let showNames = ValuePromise<Bool>(true)
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) {
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) {
self.controller = controller
self.environment = environment
self.presentationData = presentationData
@ -446,6 +464,13 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
self.actionButtonNode.titleNode.displaysAsynchronously = false
self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted)
if let startAtTimestamp = mediaParameters?.startAtTimestamp {
//TODO:localize
self.startAtTimestampNode = ShareStartAtTimestampNode(titleText: "Start at \(textForTimeout(value: startAtTimestamp))", titleTextColor: self.presentationData.theme.actionSheet.secondaryTextColor, checkNodeTheme: CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false))
} else {
self.startAtTimestampNode = nil
}
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment)
self.inputFieldNode.text = presetText ?? ""
self.inputFieldNode.preselectText()
@ -650,6 +675,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
self.contentContainerNode.addSubnode(self.actionsBackgroundNode)
self.contentContainerNode.addSubnode(self.inputFieldNode)
self.contentContainerNode.addSubnode(self.actionButtonNode)
if let startAtTimestampNode = self.startAtTimestampNode {
self.contentContainerNode.addSubnode(startAtTimestampNode)
}
self.inputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
@ -844,6 +872,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
self.actionButtonNode.badgeBackgroundColor = presentationData.theme.actionSheet.controlAccentColor
self.actionButtonNode.badgeTextColor = presentationData.theme.actionSheet.opaqueItemBackgroundColor
if let startAtTimestampNode = self.startAtTimestampNode {
startAtTimestampNode.titleTextColor = presentationData.theme.actionSheet.secondaryTextColor
startAtTimestampNode.checkNodeTheme = CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false)
}
self.contentNode?.updateTheme(presentationData.theme)
}
@ -992,6 +1025,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
actionButtonHeight = buttonHeight
bottomGridInset += actionButtonHeight
}
if self.startAtTimestampNode != nil {
bottomGridInset += buttonHeight
}
let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition)
if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil {
@ -1013,6 +1049,10 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
if let startAtTimestampNode = self.startAtTimestampNode {
transition.updateFrame(node: startAtTimestampNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
}
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: inputHeight)), beginWithCurrentState: true)
transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)), beginWithCurrentState: true)
@ -1563,6 +1603,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) {
return result
}
if let startAtTimestampNode = self.startAtTimestampNode {
if let result = startAtTimestampNode.hitTest(startAtTimestampNode.convert(point, from: self), with: event) {
return result
}
}
if self.bounds.contains(point) {
if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 {
if let result = contentInfoView.hitTest(self.view.convert(point, to: contentInfoView), with: event) {

View File

@ -363,7 +363,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.contentOffsetUpdated = f
}
private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) {
private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) {
let itemCount = self.entries.count
let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
@ -384,7 +384,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
}
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0)
let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset)
return (gridTopInset, itemWidth)
}
@ -572,7 +572,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.overrideGridOffsetTransition = nil
}
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size)
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset)
var scrollToItem: GridNodeScrollToItem?
if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {

View File

@ -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<Api.Updates>) {
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<Api.Updates>) {
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() {

View File

@ -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<PeerView?, NoError>
@ -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() {

View File

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

View File

@ -63,6 +63,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
var back: (() -> Void)?
var presentCallRating: ((CallId, Bool) -> Void)?
var callEnded: ((Bool) -> Void)?
var willBeDismissedInteractively: (() -> Void)?
var dismissedInteractively: (() -> Void)?
var present: ((ViewController) -> Void)?
var dismissAllTooltips: (() -> Void)?

View File

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

View File

@ -23,9 +23,6 @@ extension VideoChatScreenComponent.View {
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
return
}
guard let peer = self.peer else {
return
}
guard let callState = self.callState else {
return
}
@ -34,7 +31,7 @@ extension VideoChatScreenComponent.View {
var items: [ContextMenuItem] = []
if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 {
if self.peer != nil, let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 {
for peer in displayAsPeers {
if peer.peer.id == callState.myPeerId {
let avatarSize = CGSize(width: 28.0, height: 28.0)
@ -98,7 +95,7 @@ extension VideoChatScreenComponent.View {
})))
var hasPermissions = true
if case let .channel(chatPeer) = peer {
if let peer = self.peer, case let .channel(chatPeer) = peer {
if case .broadcast = chatPeer.info {
hasPermissions = false
} else if chatPeer.flags.contains(.isGigagroup) {
@ -152,54 +149,56 @@ extension VideoChatScreenComponent.View {
}
}
let qualityList: [(Int, String)] = [
(0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly),
(180, "180p"),
(360, "360p"),
(Int.max, "720p")
]
let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? ""
items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in
return nil
}, action: { [weak self] c, _ in
guard let self else {
c?.dismiss(completion: nil)
return
}
if let members = self.members, members.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) {
let qualityList: [(Int, String)] = [
(0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly),
(180, "180p"),
(360, "360p"),
(Int.max, "720p")
]
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { (c, _) in
c?.popItems()
})))
items.append(.separator)
for (quality, title) in qualityList {
let isSelected = self.maxVideoQuality == quality
items.append(.action(ContextMenuActionItem(text: title, icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
if self.maxVideoQuality != quality {
self.maxVideoQuality = quality
self.state?.updated(transition: .immediate)
}
let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? ""
items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in
return nil
}, action: { [weak self] c, _ in
guard let self else {
c?.dismiss(completion: nil)
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { (c, _) in
c?.popItems()
})))
}
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
})))
items.append(.separator)
for (quality, title) in qualityList {
let isSelected = self.maxVideoQuality == quality
items.append(.action(ContextMenuActionItem(text: title, icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
if self.maxVideoQuality != quality {
self.maxVideoQuality = quality
self.state?.updated(transition: .immediate)
}
})))
}
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
})))
}
if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
if component.call.hasScreencast {

View File

@ -205,6 +205,7 @@ private var declaredEncodables: Void = {
declareEncodable(WallpaperDataResource.self, f: { WallpaperDataResource(decoder: $0) })
declareEncodable(ForwardOptionsMessageAttribute.self, f: { ForwardOptionsMessageAttribute(decoder: $0) })
declareEncodable(SendAsMessageAttribute.self, f: { SendAsMessageAttribute(decoder: $0) })
declareEncodable(ForwardVideoTimestampAttribute.self, f: { ForwardVideoTimestampAttribute(decoder: $0) })
declareEncodable(AudioTranscriptionMessageAttribute.self, f: { AudioTranscriptionMessageAttribute(decoder: $0) })
declareEncodable(NonPremiumMessageAttribute.self, f: { NonPremiumMessageAttribute(decoder: $0) })
declareEncodable(TelegramExtendedMedia.self, f: { TelegramExtendedMedia(decoder: $0) })

View File

@ -326,35 +326,34 @@ struct ParsedMessageWebpageAttributes {
var isSafe: Bool
}
func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?) {
func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?, videoTimestamp: Int32?) {
if let media = media {
switch media {
case let .messageMediaPhoto(flags, photo, ttlSeconds):
if let photo = photo {
if let mediaImage = telegramMediaImageFromApiPhoto(photo) {
return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil)
return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil, nil)
}
} else {
return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil)
return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil, nil)
}
case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId):
let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard)
return (mediaContact, nil, nil, nil, nil)
return (mediaContact, nil, nil, nil, nil, nil)
case let .messageMediaGeo(geo):
let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil)
return (mediaMap, nil, nil, nil, nil)
return (mediaMap, nil, nil, nil, nil, nil)
case let .messageMediaVenue(geo, title, address, provider, venueId, venueType):
let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil)
return (mediaMap, nil, nil, nil, nil)
return (mediaMap, nil, nil, nil, nil, nil)
case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius):
let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading)
return (mediaMap, nil, nil, nil, nil)
return (mediaMap, nil, nil, nil, nil, nil)
case let .messageMediaDocument(flags, document, altDocuments, coverPhoto, videoTimestamp, ttlSeconds):
let _ = videoTimestamp
if let document = document {
if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments, videoCover: coverPhoto) {
return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil)
return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil, videoTimestamp)
}
} else {
var data: TelegramMediaExpiredContentData
@ -365,7 +364,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
} else {
data = .file
}
return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil)
return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil, nil)
}
case let .messageMediaWebPage(flags, webpage):
if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage) {
@ -380,14 +379,14 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
forceLargeMedia: webpageForceLargeMedia,
isManuallyAdded: (flags & (1 << 3)) != 0,
isSafe: (flags & (1 << 4)) != 0
))
), nil)
}
case .messageMediaUnsupported:
return (TelegramMediaUnsupported(), nil, nil, nil, nil)
return (TelegramMediaUnsupported(), nil, nil, nil, nil, nil)
case .messageMediaEmpty:
break
case let .messageMediaGame(game):
return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil)
return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil, nil)
case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia):
var parsedFlags = TelegramMediaInvoiceFlags()
if (flags & (1 << 3)) != 0 {
@ -396,7 +395,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
if (flags & (1 << 1)) != 0 {
parsedFlags.insert(.shippingAddressRequested)
}
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil)
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil, nil)
case let .messageMediaPoll(poll, results):
switch poll {
case let .poll(id, flags, question, answers, closePeriod, _):
@ -421,13 +420,13 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
questionEntities = messageTextEntitiesFromApiEntities(entities)
}
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil)
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil, nil)
}
case let .messageMediaDice(value, emoticon):
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil)
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil)
case let .messageMediaStory(flags, peerId, id, _):
let isMention = (flags & (1 << 1)) != 0
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil)
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil, nil)
case let .messageMediaGiveaway(apiFlags, channels, countries, prizeDescription, quantity, months, stars, untilDate):
var flags: TelegramMediaGiveaway.Flags = []
if (apiFlags & (1 << 0)) != 0 {
@ -439,9 +438,9 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
} else if let stars {
prize = .stars(amount: stars)
} else {
return (nil, nil, nil, nil, nil)
return (nil, nil, nil, nil, nil, nil)
}
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil)
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil)
case let .messageMediaGiveawayResults(apiFlags, channelId, additionalPeersCount, launchMsgId, winnersCount, unclaimedCount, winners, months, stars, prizeDescription, untilDate):
var flags: TelegramMediaGiveawayResults.Flags = []
if (apiFlags & (1 << 0)) != 0 {
@ -456,15 +455,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
} else if let stars {
prize = .stars(amount: stars)
} else {
return (nil, nil, nil, nil, nil)
return (nil, nil, nil, nil, nil, nil)
}
return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil)
return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil)
case let .messageMediaPaidMedia(starsAmount, apiExtendedMedia):
return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil)
return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil, nil)
}
}
return (nil, nil, nil, nil, nil)
return (nil, nil, nil, nil, nil, nil)
}
func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? {
@ -811,7 +810,7 @@ extension StoreMessage {
var consumableContent: (Bool, Bool)? = nil
if let media = media {
let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes, videoTimestamp) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let mediaValue = mediaValue {
medias.append(mediaValue)
@ -828,6 +827,10 @@ extension StoreMessage {
attributes.append(MediaSpoilerMessageAttribute())
}
if let videoTimestamp {
attributes.append(ForwardVideoTimestampAttribute(timestamp: videoTimestamp))
}
if mediaValue is TelegramMediaWebpage {
let leadingPreview = (flags & (1 << 27)) != 0

View File

@ -16,8 +16,7 @@ extension TelegramExtendedMedia {
}
self = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
case let .messageExtendedMedia(apiMedia):
let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId)
if let media = media {
if let media = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId).media {
self = .full(media: media)
} else {
return nil

View File

@ -252,6 +252,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
return true
case _ as EffectMessageAttribute:
return true
case _ as ForwardVideoTimestampAttribute:
return true
default:
return false
}
@ -950,6 +952,12 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
forwardInfo = nil
}
}
for attribute in requestedAttributes {
if attribute is ForwardVideoTimestampAttribute {
attributes.append(attribute)
}
}
} else {
attributes.append(contentsOf: filterMessageAttributesForOutgoingMessage(sourceMessage.attributes))
}

View File

@ -229,7 +229,19 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
}
|> mapToSignal { validatedResource -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
if let validatedResource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = validatedResource.fileReference {
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: 0, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, videoTimestamp: nil, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)))
var flags: Int32 = 0
var videoTimestamp: Int32?
for attribute in attributes {
if let attribute = attribute as? ForwardVideoTimestampAttribute {
videoTimestamp = attribute.timestamp
}
}
if videoTimestamp != nil {
flags |= 1 << 4
}
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, videoTimestamp: videoTimestamp, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else {
return .fail(.generic)
}
@ -239,14 +251,18 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
var flags: Int32 = 0
var emojiSearchQuery: String?
var videoTimestamp: Int32?
for attribute in attributes {
if let attribute = attribute as? EmojiSearchQueryMessageAttribute {
emojiSearchQuery = attribute.query
flags |= (1 << 1)
} else if let attribute = attribute as? ForwardVideoTimestampAttribute {
flags |= (1 << 4)
videoTimestamp = attribute.timestamp
}
}
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, videoTimestamp: nil, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil)))
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, videoTimestamp: videoTimestamp, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil)))
}
} else {
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: false, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
@ -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<PendingMessageUploadedContentResult, PendingMessageUploadError> in
if let inputPeer = inputPeer {
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: nil, ttlSeconds: ttlSeconds)))
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: videoCoverPhoto, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds)))
|> mapError { _ -> PendingMessageUploadError in return .generic }
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
switch result {
@ -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 {

View File

@ -324,6 +324,7 @@ private func sendUploadedMessageContent(
}
var replyToStoryId: StoryId?
var scheduleTime: Int32?
var videoTimestamp: Int32?
var sendAsPeerId: PeerId?
var bubbleUpEmojiOrStickersets = false
@ -360,6 +361,9 @@ private func sendUploadedMessageContent(
scheduleTime = attribute.scheduleTime
} else if let attribute = attribute as? SendAsMessageAttribute {
sendAsPeerId = attribute.peerId
} else if let attribute = attribute as? ForwardVideoTimestampAttribute {
flags |= Int32(1 << 20)
videoTimestamp = attribute.timestamp
}
}
@ -442,7 +446,7 @@ private func sendUploadedMessageContent(
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag)
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))

View File

@ -1159,7 +1159,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
let messageText = text
var medias: [Media] = []
let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes, videoTimestamp) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let mediaValue = mediaValue {
medias.append(mediaValue)
@ -1172,6 +1172,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
if let expirationTimer = expirationTimer {
attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil))
}
if let videoTimestamp {
attributes.append(ForwardVideoTimestampAttribute(timestamp: videoTimestamp))
}
if let nonPremium = nonPremium, nonPremium {
attributes.append(NonPremiumMessageAttribute())

View File

@ -152,7 +152,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
forwardInfo = updatedMessage.forwardInfo
threadId = updatedMessage.threadId
} else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result {
let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
let (mediaValue, _, nonPremium, hasSpoiler, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
if let mediaValue = mediaValue {
media = [mediaValue]
} else {

View File

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

View File

@ -0,0 +1,18 @@
import Foundation
import Postbox
public class ForwardVideoTimestampAttribute: MessageAttribute {
public let timestamp: Int32
public init(timestamp: Int32) {
self.timestamp = timestamp
}
required public init(decoder: PostboxDecoder) {
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.timestamp, forKey: "timestamp")
}
}

View File

@ -0,0 +1,18 @@
import Foundation
import Postbox
public class LocalMediaPlaybackInfoAttribute: MessageAttribute {
public let data: Data
public init(data: Data) {
self.data = data
}
required public init(decoder: PostboxDecoder) {
self.data = decoder.decodeDataForKey("d") ?? Data()
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeData(self.data, forKey: "d")
}
}

View File

@ -140,6 +140,7 @@ public struct Namespaces {
public static let cachedPremiumGiftCodeOptions: Int8 = 42
public static let cachedProfileGifts: Int8 = 43
public static let recommendedBots: Int8 = 44
public static let channelsForPublicReaction: Int8 = 45
}
public struct UnorderedItemList {

View File

@ -113,10 +113,10 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
break
}
}
var derivedData: DerivedDataMessageAttribute?
var previousDerivedData: DerivedDataMessageAttribute?
for attribute in previous {
if let attribute = attribute as? DerivedDataMessageAttribute {
derivedData = attribute
previousDerivedData = attribute
break
}
}
@ -134,17 +134,16 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
updated.append(audioTranscription)
}
}
if let derivedData = derivedData {
if let previousDerivedData {
var found = false
for i in 0 ..< updated.count {
if let attribute = updated[i] as? DerivedDataMessageAttribute {
updated[i] = derivedData
if let _ = updated[i] as? DerivedDataMessageAttribute {
found = true
break
}
}
if !found {
updated.append(derivedData)
updated.append(previousDerivedData)
}
}
},

View File

@ -468,7 +468,7 @@ private class AdMessagesHistoryContextImpl {
}
let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) }
let (contentMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let contentMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media
parsedMessages.append(CachedMessage(
opaqueId: randomId.makeData(),

View File

@ -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<Api.Updates?, NoError> in
return .single(nil)

View File

@ -1250,7 +1250,7 @@ func _internal_uploadStoryImpl(
}
id = idValue
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId)
let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, toPeerId).media
if let parsedMedia = parsedMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile)
}
@ -1593,7 +1593,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng
if case let .updateStory(_, story) = update {
switch story {
case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _):
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, account.peerId).media
if let parsedMedia = parsedMedia, let originalMedia = originalMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false, skipPreviews: updatingCoverTime)
}
@ -2018,7 +2018,7 @@ extension Stories.StoredItem {
init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) {
switch apiStoryItem {
case let .storyItem(flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction):
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media
if let parsedMedia = parsedMedia {
var parsedPrivacy: Stories.Item.Privacy?
if let privacy = privacy {

View File

@ -589,6 +589,28 @@ public extension TelegramEngine {
|> ignoreValues
}
public func updateLocallyDerivedData(messageId: MessageId, update: @escaping ([String: CodableEntry]) -> [String: CodableEntry]) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes
var data: [String: CodableEntry] = [:]
if let index = attributes.firstIndex(where: { $0 is DerivedDataMessageAttribute }) {
data = (attributes[index] as? DerivedDataMessageAttribute)?.data ?? [:]
attributes.remove(at: index)
}
data = update(data)
if !data.isEmpty {
attributes.append(DerivedDataMessageAttribute(data: data))
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
|> ignoreValues
}
public func rateAudioTranscription(messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
return _internal_rateAudioTranscription(postbox: self.account.postbox, network: self.account.network, messageId: messageId, id: id, isGood: isGood)
}

View File

@ -583,21 +583,25 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann
final class CachedStorySendAsPeers: Codable {
public let peerIds: [PeerId]
public let timestamp: Double
public init(peerIds: [PeerId]) {
public init(peerIds: [PeerId], timestamp: Double) {
self.peerIds = peerIds
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = try container.decode([Int64].self, forKey: "l").map(PeerId.init)
self.timestamp = try container.decodeIfPresent(Double.self, forKey: "ts") ?? 0.0
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l")
try container.encode(self.timestamp, forKey: "ts")
}
}
@ -644,7 +648,7 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> {
}
}
if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id))) {
if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id), timestamp: CFAbsoluteTimeGetCurrent())) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)), entry: entry)
}
@ -660,6 +664,77 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> {
}
}
func _internal_channelsForPublicReaction(account: Account, useLocalCache: Bool) -> Signal<[Peer], NoError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> ([Peer], Double)? in
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.channelsForPublicReaction, key: ValueBoxKey(length: 0)))?.get(CachedStorySendAsPeers.self) {
return (entry.peerIds.compactMap(transaction.getPeer), entry.timestamp)
} else {
return nil
}
}
|> mapToSignal { cachedPeers in
let remote: Signal<[Peer], NoError> = account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: 0))
|> retryRequest
|> mapToSignal { result -> Signal<[Peer], NoError> in
return account.postbox.transaction { transaction -> [Peer] in
let chats: [Api.Chat]
let parsedPeers: AccumulatedPeers
switch result {
case let .chats(apiChats):
chats = apiChats
case let .chatsSlice(_, apiChats):
chats = apiChats
}
parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _) = chat, let participantsCount = participantsCount {
transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in
var current = current as? CachedChannelData ?? CachedChannelData()
var participantsSummary = current.participantsSummary
participantsSummary.memberCount = participantsCount
current = current.withUpdatedParticipantsSummary(participantsSummary)
return current
})
}
}
}
if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id), timestamp: CFAbsoluteTimeGetCurrent())) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.channelsForPublicReaction, key: ValueBoxKey(length: 0)), entry: entry)
}
return peers
}
}
if useLocalCache {
if let cachedPeers {
return .single(cachedPeers.0)
} else {
return .single([])
}
}
if let cachedPeers {
if CFAbsoluteTimeGetCurrent() < cachedPeers.1 + 5 * 60 {
return .single(cachedPeers.0)
} else {
return .single(cachedPeers.0) |> then(remote)
}
} else {
return remote
}
}
}
public enum ChannelAddressNameAssignmentAvailability {
case available
case unknown

View File

@ -144,6 +144,13 @@ public extension TelegramEngine {
return peers.map(EnginePeer.init)
}
}
public func channelsForPublicReaction(useLocalCache: Bool) -> Signal<[EnginePeer], NoError> {
return _internal_channelsForPublicReaction(account: self.account, useLocalCache: useLocalCache)
|> map { peers -> [EnginePeer] in
return peers.map(EnginePeer.init)
}
}
public func channelAddressNameAssignmentAvailability(peerId: PeerId?) -> Signal<ChannelAddressNameAssignmentAvailability, NoError> {
return _internal_channelAddressNameAssignmentAvailability(account: self.account, peerId: peerId)

View File

@ -0,0 +1,104 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import UIKitRuntimeUtils
import AppBundle
final class ConferenceButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol {
private struct Params: Equatable {
var size: CGSize
init(size: CGSize) {
self.size = size
}
}
private let backdropBackgroundView: RoundedCornersView
private let iconView: UIImageView
var pressAction: (() -> Void)?
private var params: Params?
let maskContents: UIView
override static var layerClass: AnyClass {
return MirroringLayer.self
}
override init(frame: CGRect) {
self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true)
self.iconView = UIImageView()
self.maskContents = UIView()
self.maskContents.addSubview(self.backdropBackgroundView)
super.init(frame: frame)
self.addSubview(self.iconView)
(self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer
self.internalHighligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut))
transition.setScale(layer: self.layer, scale: topScale)
} else {
let t = self.layer.presentation()?.transform ?? layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
let transition = ComponentTransition(animation: .none)
transition.setScale(layer: self.layer, scale: 1.0)
self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.pressAction?()
}
func update(size: CGSize, transition: ComponentTransition) {
let params = Params(size: size)
if self.params == params {
return
}
self.params = params
self.update(params: params, transition: transition)
}
private func update(params: Params, transition: ComponentTransition) {
self.backdropBackgroundView.update(cornerRadius: params.size.height * 0.5, transition: transition)
transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
if self.iconView.image == nil {
self.iconView.image = UIImage(bundleImageName: "Contact List/AddMemberIcon")?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = .white
}
if let image = self.iconView.image {
let fraction: CGFloat = 1.0
let imageSize = CGSize(width: floor(image.size.width * fraction), height: floor(image.size.height * fraction))
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((params.size.width - imageSize.width) * 0.5), y: floorToScreenPixels((params.size.height - imageSize.height) * 0.5)), size: imageSize))
}
}
}

View File

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

View File

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

View File

@ -33,6 +33,7 @@ import WallpaperPreviewMedia
import TextNodeWithEntities
import RangeSet
import GiftItemComponent
import MediaResources
private struct FetchControls {
let fetch: (Bool) -> Void
@ -444,6 +445,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
public let dateAndStatusNode: ChatMessageDateAndStatusNode
private var badgeNode: ChatMessageInteractiveMediaBadge?
private var timestampContainerView: UIView?
private var timestampMaskView: UIImageView?
private var videoTimestampBackgroundLayer: SimpleLayer?
private var videoTimestampForegroundLayer: SimpleLayer?
private var extendedMediaOverlayNode: ExtendedMediaOverlayNode?
private var context: AccountContext?
@ -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)

View File

@ -21,6 +21,7 @@ import BundleIconComponent
import CheckNode
import TextFormat
import CheckComponent
import ContextUI
private final class BalanceComponent: CombinedComponent {
let context: AccountContext
@ -821,6 +822,8 @@ private final class ChatSendStarsScreenComponent: Component {
let context: AccountContext
let peer: EnginePeer
let myPeer: EnginePeer
let sendAsPeer: EnginePeer
let channelsForPublicReaction: [EnginePeer]
let messageId: EngineMessage.Id
let maxAmount: Int
let balance: StarsAmount?
@ -833,6 +836,8 @@ private final class ChatSendStarsScreenComponent: Component {
context: AccountContext,
peer: EnginePeer,
myPeer: EnginePeer,
sendAsPeer: EnginePeer,
channelsForPublicReaction: [EnginePeer],
messageId: EngineMessage.Id,
maxAmount: Int,
balance: StarsAmount?,
@ -844,6 +849,8 @@ private final class ChatSendStarsScreenComponent: Component {
self.context = context
self.peer = peer
self.myPeer = myPeer
self.sendAsPeer = sendAsPeer
self.channelsForPublicReaction = channelsForPublicReaction
self.messageId = messageId
self.maxAmount = maxAmount
self.balance = balance
@ -863,6 +870,12 @@ private final class ChatSendStarsScreenComponent: Component {
if lhs.myPeer != rhs.myPeer {
return false
}
if lhs.sendAsPeer != rhs.sendAsPeer {
return false
}
if lhs.channelsForPublicReaction != rhs.channelsForPublicReaction {
return false
}
if lhs.maxAmount != rhs.maxAmount {
return false
}
@ -988,6 +1001,7 @@ private final class ChatSendStarsScreenComponent: Component {
private let hierarchyTrackingNode: HierarchyTrackingNode
private let leftButton = ComponentView<Empty>()
private let peerSelectorButton = ComponentView<Empty>()
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
@ -1037,6 +1051,10 @@ private final class ChatSendStarsScreenComponent: Component {
private var badgePhysicsLink: SharedDisplayLinkDriver.Link?
private var currentMyPeer: EnginePeer?
private var channelsForPublicReaction: [EnginePeer] = []
private var channelsForPublicReactionDisposable: Disposable?
override init(frame: CGRect) {
self.bottomOverscrollLimit = 200.0
@ -1119,6 +1137,7 @@ private final class ChatSendStarsScreenComponent: Component {
deinit {
self.balanceDisposable?.dispose()
self.channelsForPublicReactionDisposable?.dispose()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -1297,6 +1316,87 @@ private final class ChatSendStarsScreenComponent: Component {
}
}
private func displayTargetSelectionMenu(sourceView: UIView) {
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
return
}
var items: [ContextMenuItem] = []
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
var peers: [EnginePeer] = [component.myPeer]
peers.append(contentsOf: self.channelsForPublicReaction)
let avatarSize = CGSize(width: 30.0, height: 30.0)
for peer in peers {
let peerLabel: String
if peer.id == component.context.account.peerId {
peerLabel = environment.strings.AffiliateProgram_PeerTypeSelf
} else if case .channel = peer {
peerLabel = environment.strings.Channel_Status
} else {
peerLabel = environment.strings.Bot_GenericBotStatus
}
let isSelected = peer.id == self.currentMyPeer?.id
let accentColor = environment.theme.list.itemAccentColor
let avatarSignal = peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize)
|> map { image in
let context = DrawingContext(size: avatarSize, scale: 0.0, clear: true)
context?.withContext { c in
UIGraphicsPushContext(c)
defer {
UIGraphicsPopContext()
}
if isSelected {
}
c.saveGState()
let scaleFactor = (avatarSize.width - 3.0 * 2.0) / avatarSize.width
if isSelected {
c.translateBy(x: avatarSize.width * 0.5, y: avatarSize.height * 0.5)
c.scaleBy(x: scaleFactor, y: scaleFactor)
c.translateBy(x: -avatarSize.width * 0.5, y: -avatarSize.height * 0.5)
}
if let image {
image.draw(in: CGRect(origin: CGPoint(), size: avatarSize))
}
c.restoreGState()
if isSelected {
c.setStrokeColor(accentColor.cgColor)
let lineWidth: CGFloat = 1.0 + UIScreenPixel
c.setLineWidth(lineWidth)
c.strokeEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
}
}
return context?.generateImage()
}
items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { [weak self] c, _ in
c?.dismiss(completion: {})
guard let self, let component = self.component else {
return
}
if self.currentMyPeer?.id == peer.id {
return
}
if self.currentMyPeer != peer {
self.currentMyPeer = peer
let _ = component.context.engine.peers.updatePeerSendAsPeer(peerId: component.peer.id, sendAs: peer.id).startStandalone()
}
self.state?.updated(transition: .immediate)
})))
}
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
@ -1312,6 +1412,8 @@ private final class ChatSendStarsScreenComponent: Component {
let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 16.0
if self.component == nil {
self.currentMyPeer = component.myPeer
self.balance = component.balance
var isLogarithmic = true
if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_stars_reaction_logarithmic_scale"] as? Double {
@ -1336,6 +1438,17 @@ private final class ChatSendStarsScreenComponent: Component {
}
})
}
self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false)
|> deliverOnMainQueue).startStrict(next: { [weak self] peers in
guard let self else {
return
}
if self.channelsForPublicReaction != peers {
self.channelsForPublicReaction = peers
self.state?.updated(transition: .immediate)
}
})
}
self.component = component
@ -1514,6 +1627,9 @@ private final class ChatSendStarsScreenComponent: Component {
contentHeight += 123.0
var sendAsPeers: [EnginePeer] = [component.myPeer]
sendAsPeers.append(contentsOf: self.channelsForPublicReaction)
let leftButtonSize = self.leftButton.update(
transition: transition,
component: AnyComponent(BalanceComponent(
@ -1531,6 +1647,35 @@ private final class ChatSendStarsScreenComponent: Component {
self.navigationBarContainer.addSubview(leftButtonView)
}
transition.setFrame(view: leftButtonView, frame: leftButtonFrame)
leftButtonView.isHidden = sendAsPeers.count > 1
}
let currentMyPeer = self.currentMyPeer ?? component.myPeer
let peerSelectorButtonSize = self.peerSelectorButton.update(
transition: transition,
component: AnyComponent(PeerSelectorBadgeComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: currentMyPeer,
action: { [weak self] sourceView in
guard let self else {
return
}
self.displayTargetSelectionMenu(sourceView: sourceView)
}
)),
environment: {},
containerSize: CGSize(width: 120.0, height: 100.0)
)
let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((56.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize)
if let peerSelectorButtonView = self.peerSelectorButton.view {
if peerSelectorButtonView.superview == nil {
self.navigationBarContainer.addSubview(peerSelectorButtonView)
}
transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame)
peerSelectorButtonView.isHidden = sendAsPeers.count <= 1
}
if themeUpdated {
@ -1717,7 +1862,7 @@ private final class ChatSendStarsScreenComponent: Component {
if myCount != 0 {
mappedTopPeers.append(ChatSendStarsScreen.TopPeer(
randomIndex: -1,
peer: self.isAnonymous ? nil : component.myPeer,
peer: self.isAnonymous ? nil : currentMyPeer,
isMy: true,
count: myCount
))
@ -2102,6 +2247,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
public final class InitialData {
fileprivate let peer: EnginePeer
fileprivate let myPeer: EnginePeer
fileprivate let sendAsPeer: EnginePeer
fileprivate let channelsForPublicReaction: [EnginePeer]
fileprivate let messageId: EngineMessage.Id
fileprivate let balance: StarsAmount?
fileprivate let currentSentAmount: Int?
@ -2111,6 +2258,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
fileprivate init(
peer: EnginePeer,
myPeer: EnginePeer,
sendAsPeer: EnginePeer,
channelsForPublicReaction: [EnginePeer],
messageId: EngineMessage.Id,
balance: StarsAmount?,
currentSentAmount: Int?,
@ -2119,6 +2268,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
) {
self.peer = peer
self.myPeer = myPeer
self.sendAsPeer = sendAsPeer
self.channelsForPublicReaction = channelsForPublicReaction
self.messageId = messageId
self.balance = balance
self.currentSentAmount = currentSentAmount
@ -2204,6 +2355,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
context: context,
peer: initialData.peer,
myPeer: initialData.myPeer,
sendAsPeer: initialData.sendAsPeer,
channelsForPublicReaction: initialData.channelsForPublicReaction,
messageId: initialData.messageId,
maxAmount: maxAmount,
balance: initialData.balance,
@ -2266,15 +2419,20 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
topPeers = Array(topPeers.prefix(3))
}
let channelsForPublicReaction = context.engine.peers.channelsForPublicReaction(useLocalCache: true)
let sendAsPeer: Signal<EnginePeer?, NoError> = .single(nil)
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
EngineDataMap(allPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
),
balance
balance,
sendAsPeer,
channelsForPublicReaction
)
|> map { peerAndTopPeerMap, balance -> InitialData? in
|> map { peerAndTopPeerMap, balance, sendAsPeer, channelsForPublicReaction -> InitialData? in
let (peer, myPeer, topPeerMap) = peerAndTopPeerMap
guard let peer, let myPeer else {
return nil
@ -2284,6 +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<Empty>()
private var avatarNode: AvatarNode?
private var selectorIcon: ComponentView<Empty>?
private var component: PeerSelectorBadgeComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action?(self)
}
func update(component: PeerSelectorBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.isEnabled = component.action != nil
let height: CGFloat = 32.0
let avatarPadding: CGFloat = 1.0
let avatarDiameter = height - avatarPadding * 2.0
let avatarTextSpacing: CGFloat = -4.0
let rightTextInset: CGFloat = component.action != nil ? 26.0 : 12.0
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(avatarDiameter * 0.5)))
avatarNode.isUserInteractionEnabled = false
avatarNode.displaysAsynchronously = false
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
}
let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter))
avatarNode.frame = avatarFrame
avatarNode.updateSize(size: avatarFrame.size)
avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, synchronousLoad: true)
let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + rightTextInset, height: height)
if component.action != nil {
let selectorIcon: ComponentView<Empty>
if let current = self.selectorIcon {
selectorIcon = current
} else {
selectorIcon = ComponentView()
self.selectorIcon = selectorIcon
}
let selectorIconSize = selectorIcon.update(
transition: transition,
component: AnyComponent(BundleIconComponent(
name: "Item List/ExpandableSelectorArrows", tintColor: component.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5))),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize)
if let selectorIconView = selectorIcon.view {
if selectorIconView.superview == nil {
selectorIconView.isUserInteractionEnabled = false
self.addSubview(selectorIconView)
}
transition.setFrame(view: selectorIconView, frame: selectorIconFrame)
}
} else if let selectorIcon = self.selectorIcon {
self.selectorIcon = nil
selectorIcon.view?.removeFromSuperview()
}
let _ = self.background.update(
transition: transition,
component: AnyComponent(FilledRoundedRectangleComponent(
color: component.theme.list.itemInputField.backgroundColor,
cornerRadius: .minEdge,
smoothCorners: false
)),
environment: {},
containerSize: size
)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
backgroundView.isUserInteractionEnabled = false
self.insertSubview(backgroundView, at: 0)
}
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size))
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceView: UIView
private let actionsOnTop: Bool
init(controller: ViewController, sourceView: UIView, actionsOnTop: Bool) {
self.controller = controller
self.sourceView = sourceView
self.actionsOnTop = actionsOnTop
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom)
}
}

View File

@ -100,6 +100,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let openBotButton = ComponentView<Empty>()
private var dailyRevenueText: ComponentView<Empty>?
private let titleTransformContainer: UIView
private let bottomPanelContainer: UIView
@ -832,6 +833,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<Empty>] = []
var openBotLeftInset: CGFloat = 12.0
if case .active = component.mode {
openBotLeftInset = 1.0
openBotComponents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents(
content: AnyComponent(AvatarComponent(
context: component.context,
peer: component.sourcePeer,
size: CGSize(width: 30.0, height: 30.0)
)), fixedSize: CGSize(width: 30.0, height: 2.0),
translation: CGPoint(x: 0.0, y: 1.0)))))
}
openBotComponents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "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(

View File

@ -6266,7 +6266,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let strongSelf = self {
let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)
let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact)), updatedPresentationData: strongSelf.controller?.updatedPresentationData)
let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact), nil), updatedPresentationData: strongSelf.controller?.updatedPresentationData)
shareController.completed = { [weak self] peerIds in
if let strongSelf = self {
let _ = (strongSelf.context.engine.data.get(

View File

@ -2599,7 +2599,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let shareController = ShareController(
context: self.context,
subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false))),
subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false)), nil),
presetText: nil,
preferredAction: .default,
showInChat: nil,

View File

@ -1035,7 +1035,7 @@ final class StoryItemSetContainerSendMessage {
let shareController = ShareController(
context: component.context,
subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false))),
subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false)), nil),
preferredAction: preferredAction ?? .default,
externalShare: false,
immediateExternalShare: false,

View File

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

View File

@ -0,0 +1,88 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.834991 2.770020 cm
0.000000 0.000000 0.000000 scn
4.135217 12.200247 m
4.010505 12.324958 3.841359 12.395020 3.664990 12.395020 c
3.488621 12.395020 3.319476 12.324956 3.194765 12.200244 c
0.194780 9.200245 l
-0.064918 8.940545 -0.064917 8.519490 0.194782 8.259792 c
0.454482 8.000094 0.875536 8.000095 1.135234 8.259794 c
3.664994 10.789568 l
6.194782 8.259792 l
6.454482 8.000094 6.875536 8.000095 7.135234 8.259794 c
7.394932 8.519494 7.394931 8.940549 7.135232 9.200247 c
4.135217 12.200247 l
h
7.135226 3.259784 m
4.135226 0.259784 l
3.875527 0.000086 3.454473 0.000086 3.194774 0.259784 c
0.194774 3.259784 l
-0.064925 3.519483 -0.064925 3.940537 0.194774 4.200236 c
0.454473 4.459935 0.875527 4.459935 1.135226 4.200236 c
3.665000 1.670462 l
6.194774 4.200236 l
6.454473 4.459935 6.875527 4.459935 7.135226 4.200236 c
7.394925 3.940537 7.394925 3.519483 7.135226 3.259784 c
h
f*
n
Q
endstream
endobj
3 0 obj
959
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 9.000000 18.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001049 00000 n
0000001071 00000 n
0000001243 00000 n
0000001317 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1376
%%EOF

View File

@ -305,7 +305,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
override func loadDisplayNode() {
self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, attemptDisabledItemSelection: self.attemptDisabledItemSelection, options: self.options, filters: self.filters, onlyWriteable: self.onlyWriteable, isGroupInvitation: self.isGroupInvitation, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit, present: { [weak self] c, a in
self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, updatedPresentationData: self.params.updatedPresentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, attemptDisabledItemSelection: self.attemptDisabledItemSelection, options: self.options, filters: self.filters, onlyWriteable: self.onlyWriteable, isGroupInvitation: self.isGroupInvitation, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
switch self.contactsNode.contentNode {

View File

@ -82,7 +82,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
private let onlyWriteable: Bool
private let isGroupInvitation: Bool
init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) {
init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) {
self.navigationBar = navigationBar
self.context = context
@ -241,7 +241,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
return .natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)
}
let contactListNode = ContactListNode(context: context, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState())
let contactListNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState())
self.contentNode = .contacts(contactListNode)
if !selectedPeers.isEmpty {
@ -365,7 +365,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
case .premiumGifting, .requestedUsersSelection:
searchChatList = true
}
let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(ContactListPresentation.Search(
let searchResultsNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: .single(.search(ContactListPresentation.Search(
signal: searchText.get(),
searchChatList: searchChatList,
searchDeviceContacts: false,
@ -373,7 +373,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
searchChannels: searchChannels,
globalSearch: globalSearch,
displaySavedMessages: displaySavedMessages
))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true)
))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true)
searchResultsNode.openPeer = { peer, _, _, _ in
self?.tokenListNode.setText("")
self?.openPeer?(peer)

View File

@ -227,7 +227,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
params.dismissInput()
let presentationData = params.context.sharedContext.currentPresentationData.with { $0 }
if immediateShare {
let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true)
let controller = ShareController(context: params.context, subject: .media(.standalone(media: file), nil), immediateExternalShare: true)
params.present(controller, nil, .window(.root))
} else if let rootController = params.navigationController?.view.window?.rootViewController {
let proceed = {

View File

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

View File

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