Merge commit 'aa46e81a4928a22f58e9b6240b31104c01767c0a'

# Conflicts:
#	Telegram/Telegram-iOS/en.lproj/Localizable.strings
#	submodules/UndoUI/Sources/UndoOverlayControllerNode.swift
This commit is contained in:
Ali 2022-07-13 01:23:56 +02:00
commit 815cb9b341
35 changed files with 971 additions and 344 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7831,6 +7831,9 @@ Sorry for the inconvenience.";
"Notification.PremiumGift.Sent" = "%1$@ sent you a gift for %2$@";
"Notification.PremiumGift.SentYou" = "You sent a gift for %@";
"Notification.PremiumGift.Months_1" = "%@ month";
"Notification.PremiumGift.Months_any" = "%@ months";
"Notification.PremiumGift.Title" = "Telegram Premium";
"Notification.PremiumGift.Subtitle" = "for %@";
"Notification.PremiumGift.View" = "View";
@ -7840,3 +7843,9 @@ Sorry for the inconvenience.";
"StickerPack.RemoveEmojiCount_1" = "Remove 1 Emoji";
"StickerPack.RemoveEmojiCount_any" = "Remove %@ Emoji";
"Gallery.AirPlay" = "AirPlay";
"Gallery.AirPlayPlaceholder" = "This video is playing on the TV using AirPlay";
"WebApp.CloseConfirmation" = "Changes that you made may not be saved.";
"WebApp.CloseAnyway" = "Close Anyway";

View File

@ -37,6 +37,9 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
var interactivelyDismissed: (() -> Void)?
var controllerRemoved: ((ViewController) -> Void)?
var shouldCancelPanGesture: (() -> Bool)?
var requestDismiss: (() -> Void)?
var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var isUpdatingState = false
@ -232,6 +235,12 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
if !self.isExpanded, translation > 40.0, let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() {
self.cancelPanGesture()
self.requestDismiss?()
return
}
var bounds = self.bounds
if self.isExpanded {
bounds.origin.y = -max(0.0, translation - edgeTopInset)
@ -277,8 +286,13 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let offset = currentTopInset + panOffset
let topInset: CGFloat = edgeTopInset
var ignoreDismiss = false
if let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() {
ignoreDismiss = true
}
var dismissing = false
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) {
if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss {
self.interactivelyDismissed?()
dismissing = true
} else if self.isExpanded {
@ -340,6 +354,12 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.isAnimating = true
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut), completion: completion)
var bounds = self.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.bounds = bounds
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
default:
break
}

View File

@ -83,6 +83,7 @@ public protocol AttachmentContainable: ViewController {
func prepareForReuse()
func requestDismiss(completion: @escaping () -> Void)
func shouldDismissImmediately() -> Bool
}
public extension AttachmentContainable {
@ -101,6 +102,10 @@ public extension AttachmentContainable {
func requestDismiss(completion: @escaping () -> Void) {
completion()
}
func shouldDismissImmediately() -> Bool {
return true
}
}
public enum AttachmentMediaPickerSendMode {
@ -312,6 +317,28 @@ public class AttachmentController: ViewController {
}
}
self.container.shouldCancelPanGesture = { [weak self] in
if let strongSelf = self, let currentController = strongSelf.currentControllers.last {
if !currentController.shouldDismissImmediately() {
return true
} else {
return false
}
} else {
return false
}
}
self.container.requestDismiss = { [weak self] in
if let strongSelf = self, let currentController = strongSelf.currentControllers.last {
currentController.requestDismiss { [weak self] in
if let strongSelf = self {
strongSelf.controller?.dismiss(animated: true)
}
}
}
}
self.panel.selectionChanged = { [weak self] type in
if let strongSelf = self {
return strongSelf.switchToController(type)

View File

@ -176,7 +176,7 @@ open class ViewControllerComponentContainer: ViewController {
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
private var presentationDataDisposable: Disposable?
private var validLayout: ContainerViewLayout?
public private(set) var validLayout: ContainerViewLayout?
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.context = context

View File

@ -222,7 +222,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
self.subtitleNode.attributedText = subtitle.flatMap { subtitle in
return NSAttributedString(
string: self.item.text,
string: subtitle,
font: subtitleFont,
textColor: subtitleColor
)

View File

@ -5,7 +5,7 @@ import AVFoundation
public extension UnicodeScalar {
var isEmoji: Bool {
switch self.value {
case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8, 0x1FA70...0x1FA73, 0x1FA78...0x1FA7A, 0x1FA80...0x1FA82, 0x1FA90...0x1FA95:
case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8, 0x1FA70...0x1FA73, 0x1FA78...0x1FA7A, 0x1FA80...0x1FA82, 0x1FA90...0x1FA95, 0x1F382:
return true
case 0x2603, 0x265F, 0x267E, 0x2692, 0x26C4, 0x26C8, 0x26CE, 0x26CF, 0x26D1...0x26D3, 0x26E9, 0x26F0...0x26F9, 0x2705, 0x270A, 0x270B, 0x2728, 0x274E, 0x2753...0x2755, 0x274C, 0x2795...0x2797, 0x27B0, 0x27BF:
return true

View File

@ -147,10 +147,15 @@ private let moreButtonImage = generateTintedImage(image: UIImage(bundleImageName
private let placeholderFont = Font.regular(16.0)
private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode {
enum Mode {
case pictureInPicture
case airplay
}
private let iconNode: ASImageNode
private let textNode: ASTextNode
init(strings: PresentationStrings) {
init(strings: PresentationStrings, mode: Mode) {
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
@ -160,10 +165,20 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: strings.Embed_PlayingInPIP, font: placeholderFont, textColor: UIColor(rgb: 0x8e8e93))
let text: String
switch mode {
case .pictureInPicture:
text = strings.Embed_PlayingInPIP
case .airplay:
text = strings.Gallery_AirPlayPlaceholder
}
self.textNode.attributedText = NSAttributedString(string: text, font: placeholderFont, textColor: UIColor(rgb: 0x8e8e93))
super.init()
self.backgroundColor = UIColor(rgb: 0x333335)
self.addSubnode(self.iconNode)
self.addSubnode(self.textNode)
}
@ -975,7 +990,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if let pictureInPictureNode = self.pictureInPictureNode {
if let item = self.item {
let placeholderSize = item.content.dimensions.fitted(layout.size)
var placeholderSize = item.content.dimensions.fitted(layout.size)
placeholderSize.height += 2.0
transition.updateFrame(node: pictureInPictureNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - placeholderSize.width) / 2.0), y: floor((layout.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
pictureInPictureNode.updateLayout(placeholderSize, transition: transition)
}
@ -1144,11 +1160,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.videoNode = videoNode
self.videoNodeUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
videoNode.isUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
videoNode.backgroundColor = UIColor.black
if item.fromPlayingVideo {
videoNode.canAttachContent = false
} else {
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
self.updateDisplayPlaceholder()
}
scrubberView.setStatusSignal(videoNode.status |> map { value -> MediaPlayerStatus in
@ -1492,21 +1508,25 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.videoNode?.notifyPlaybackControlsHidden(!isVisible)
}
private func updateDisplayPlaceholder() {
self.updateDisplayPlaceholder(!(self.videoNode?.ownsContentNode ?? true) || self.isAirPlayActive)
}
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
if displayPlaceholder && !self.disablePictureInPicturePlaceholder {
if self.pictureInPictureNode == nil {
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings)
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings, mode: self.isAirPlayActive ? .airplay : .pictureInPicture)
pictureInPictureNode.isUserInteractionEnabled = false
self.pictureInPictureNode = pictureInPictureNode
self.insertSubnode(pictureInPictureNode, aboveSubnode: self.scrollNode)
if let validLayout = self.validLayout {
if let item = self.item {
let placeholderSize = item.content.dimensions.fitted(validLayout.0.size)
pictureInPictureNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.0.size.width - placeholderSize.width) / 2.0), y: floor((validLayout.0.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)
var placeholderSize = item.content.dimensions.fitted(validLayout.0.size)
placeholderSize.height += 2.0
pictureInPictureNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.0.size.width - placeholderSize.width) / 2.0), y: floorToScreenPixels((validLayout.0.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)
pictureInPictureNode.updateLayout(placeholderSize, transition: .immediate)
}
}
self.videoNode?.backgroundColor = UIColor(rgb: 0x333335)
}
} else if let pictureInPictureNode = self.pictureInPictureNode {
self.pictureInPictureNode = nil
@ -1602,10 +1622,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} else {
videoNode.continuePlayingWithoutSound()
}
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
self.updateDisplayPlaceholder()
} else if !item.fromPlayingVideo {
videoNode.canAttachContent = isVisible
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
self.updateDisplayPlaceholder()
}
if self.shouldAutoplayOnCentrality() {
self.hideStatusNodeUntilCentrality = true
@ -1690,7 +1710,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
videoNode.canAttachContent = true
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
self.updateDisplayPlaceholder()
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
} else {
@ -1768,7 +1788,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if self.item?.fromPlayingVideo ?? false {
Queue.mainQueue().after(0.001) {
videoNode.canAttachContent = true
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
self.updateDisplayPlaceholder()
}
}
@ -2457,6 +2477,16 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
c.setItems(strongSelf.contextMenuSpeedItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
})))
if #available(iOS 11.0, *) {
items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let strongSelf = self else {
return
}
strongSelf.beginAirPlaySetup()
})))
}
if let (message, _, _) = strongSelf.contentInfo() {
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
@ -2578,63 +2608,85 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return items
}
}
private var isAirPlayActive = false
private var externalVideoPlayer: ExternalVideoPlayer?
func beginAirPlaySetup() {
guard let content = self.item?.content as? NativeVideoContent else {
return
}
if #available(iOS 11.0, *) {
self.externalVideoPlayer = ExternalVideoPlayer(context: self.context, content: content)
self.externalVideoPlayer?.openRouteSelection()
self.externalVideoPlayer?.isActiveUpdated = { [weak self] isActive in
if let strongSelf = self {
if strongSelf.isAirPlayActive && !isActive {
strongSelf.externalVideoPlayer = nil
}
strongSelf.isAirPlayActive = isActive
strongSelf.updateDisplayPlaceholder()
}
}
}
}
@objc func openStickersButtonPressed() {
if let content = self.item?.content as? NativeVideoContent {
let context = self.context
let media = content.fileReference.abstract
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let topController = (self.baseNavigationController()?.topViewController as? ViewController)
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
topController?.present(controller, in: .window(.root), with: nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
self.isInteractingPromise.set(true)
let signal = self.context.engine.stickers.stickerPacksAttachedToMedia(media: media)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { [weak self] packs in
guard let strongSelf = self, !packs.isEmpty else {
return
}
let baseNavigationController = strongSelf.baseNavigationController()
baseNavigationController?.view.endEditing(true)
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { info, items, action in
let animateInAsReplacement = false
switch action {
case .add:
topController?.present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { _ in
return true
}), in: .window(.root))
case let .remove(positionInList):
topController?.present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in
if case .undo = action {
let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start()
}
return true
}), in: .window(.root))
}
}, dismissed: { [weak self] in
self?.isInteractingPromise.set(false)
})
(baseNavigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil)
})
guard let content = self.item?.content as? NativeVideoContent else {
return
}
let context = self.context
let media = content.fileReference.abstract
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let topController = (self.baseNavigationController()?.topViewController as? ViewController)
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
topController?.present(controller, in: .window(.root), with: nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
self.isInteractingPromise.set(true)
let signal = self.context.engine.stickers.stickerPacksAttachedToMedia(media: media)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { [weak self] packs in
guard let strongSelf = self, !packs.isEmpty else {
return
}
let baseNavigationController = strongSelf.baseNavigationController()
baseNavigationController?.view.endEditing(true)
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { info, items, action in
let animateInAsReplacement = false
switch action {
case .add:
topController?.present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { _ in
return true
}), in: .window(.root))
case let .remove(positionInList):
topController?.present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in
if case .undo = action {
let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start()
}
return true
}), in: .window(.root))
}
}, dismissed: { [weak self] in
self?.isInteractingPromise.set(false)
})
(baseNavigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil)
})
}
override func adjustForPreviewing() {

View File

@ -1335,6 +1335,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
public func shouldDismissImmediately() -> Bool {
if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 {
return false
} else {
return true
}
}
@objc private func cancelPressed() {
self.dismissAllTooltips()

View File

@ -50,11 +50,11 @@ private func generateDiffuseTexture() -> UIImage {
class GiftAvatarComponent: Component {
let context: AccountContext
let peer: EnginePeer
let peer: EnginePeer?
let isVisible: Bool
let hasIdleAnimations: Bool
init(context: AccountContext, peer: EnginePeer, isVisible: Bool, hasIdleAnimations: Bool) {
init(context: AccountContext, peer: EnginePeer?, isVisible: Bool, hasIdleAnimations: Bool) {
self.context = context
self.peer = peer
self.isVisible = isVisible
@ -62,7 +62,7 @@ class GiftAvatarComponent: Component {
}
static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
return lhs.peer == rhs.peer && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -276,7 +276,9 @@ class GiftAvatarComponent: Component {
self.hasIdleAnimations = component.hasIdleAnimations
let avatarSize = CGSize(width: 100.0, height: 100.0)
self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: component.peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
if let peer = component.peer {
self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
}
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize)
return availableSize

View File

@ -500,14 +500,14 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
let context: AccountContext
let peer: EnginePeer
let products: [InAppPurchaseManager.Product]
let selectedProductId: String
let peer: EnginePeer?
let products: [InAppPurchaseManager.Product]?
let selectedProductId: String?
let present: (ViewController) -> Void
let selectProduct: (String) -> Void
init(context: AccountContext, peer: EnginePeer, products: [InAppPurchaseManager.Product], selectedProductId: String, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void) {
init(context: AccountContext, peer: EnginePeer?, products: [InAppPurchaseManager.Product]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void) {
self.context = context
self.peer = peer
self.products = products
@ -538,7 +538,6 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
let fade = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self)
let section = Child(ProductGroupComponent.self)
let termsText = Child(MultilineTextComponent.self)
return { context in
let sideInset: CGFloat = 16.0
@ -584,7 +583,6 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
let textColor = theme.list.itemPrimaryTextColor
let subtitleColor = theme.list.itemSecondaryTextColor
// let arrowColor = theme.list.disclosureArrowColor
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
@ -595,7 +593,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
let text = text.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.Premium_Gift_Description(component.peer.compactDisplayTitle).string,
text: strings.Premium_Gift_Description(component.peer?.compactDisplayTitle ?? "").string,
attributes: markdownAttributes
),
horizontalAlignment: .center,
@ -621,52 +619,54 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
]
var i = 0
for product in component.products {
let monthsCount: Int
let giftTitle: String
let discount: String
switch product.id {
case "org.telegram.telegramPremium.twelveMonths":
giftTitle = strings.Premium_Gift_Years(1)
monthsCount = 12
discount = "-15%"
case "org.telegram.telegramPremium.sixMonths":
giftTitle = strings.Premium_Gift_Months(6)
monthsCount = 6
discount = "-10%"
case "org.telegram.telegramPremium.threeMonths":
giftTitle = strings.Premium_Gift_Months(3)
monthsCount = 3
discount = "-7%"
default:
giftTitle = ""
monthsCount = 1
discount = ""
}
items.append(ProductGroupComponent.Item(
AnyComponentWithIdentity(
id: product.id,
component: AnyComponent(
GiftComponent(
title: giftTitle,
totalPrice: product.price,
perMonthPrice: strings.Premium_Gift_PricePerMonth(product.pricePerMonth(monthsCount)).string,
discount: discount,
selected: product.id == component.selectedProductId,
primaryTextColor: textColor,
secondaryTextColor: subtitleColor,
accentColor: gradientColors[i],
checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor,
checkBorderColor: environment.theme.list.itemCheckColors.strokeColor
if let products = component.products {
for product in products {
let monthsCount: Int
let giftTitle: String
let discount: String
switch product.id {
case "org.telegram.telegramPremium.twelveMonths":
giftTitle = strings.Premium_Gift_Years(1)
monthsCount = 12
discount = "-15%"
case "org.telegram.telegramPremium.sixMonths":
giftTitle = strings.Premium_Gift_Months(6)
monthsCount = 6
discount = "-10%"
case "org.telegram.telegramPremium.threeMonths":
giftTitle = strings.Premium_Gift_Months(3)
monthsCount = 3
discount = "-7%"
default:
giftTitle = ""
monthsCount = 1
discount = ""
}
items.append(ProductGroupComponent.Item(
AnyComponentWithIdentity(
id: product.id,
component: AnyComponent(
GiftComponent(
title: giftTitle,
totalPrice: product.price,
perMonthPrice: strings.Premium_Gift_PricePerMonth(product.pricePerMonth(monthsCount)).string,
discount: discount,
selected: product.id == component.selectedProductId,
primaryTextColor: textColor,
secondaryTextColor: subtitleColor,
accentColor: gradientColors[i],
checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor,
checkBorderColor: environment.theme.list.itemCheckColors.strokeColor
)
)
)
),
action: {
component.selectProduct(product.id)
})
)
i += 1
),
action: {
component.selectProduct(product.id)
})
)
i += 1
}
}
let section = section.update(
@ -687,55 +687,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
size.height += section.size.height
size.height += 23.0
let textSideInset: CGFloat = 16.0
let termsFont = Font.regular(13.0)
let termsTextColor = environment.theme.list.freeTextColor
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let termsString: MultilineTextComponent.TextContent = .markdown(
text: strings.Premium_Gift_Info,
attributes: termsMarkdownAttributes
)
let accountContext = component.context
let peerId = component.peer.id
let present = component.present
let termsText = termsText.update(
component: MultilineTextComponent(
text: termsString,
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
let controller = PremiumIntroScreen(context: accountContext, source: .profile(peerId))
present(controller)
}
}
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
// context.add(termsText
// .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0))
// )
context.add(termsText
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + 164.0 + termsText.size.height / 2.0))
)
size.height += termsText.size.height
size.height += 10.0
size.height += scrollEnvironment.insets.bottom
@ -848,11 +800,11 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
let duration: Int32
switch product.id {
case "org.telegram.telegramPremium.twelveMonths":
duration = 86400 * 365
duration = 12
case "org.telegram.telegramPremium.sixMonths":
duration = 86400 * 180
duration = 6
case "org.telegram.telegramPremium.threeMonths":
duration = 86400 * 90
duration = 3
default:
duration = 0
}
@ -885,7 +837,6 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
// strongSelf.completion(duration)
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
@ -896,12 +847,14 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
}
}, completed: { [weak self] in
if let strongSelf = self {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion(duration)
Queue.mainQueue().after(2.0) {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion(duration)
}
}
}))
}
@ -958,6 +911,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
let bottomPanel = Child(BlurredRectangle.self)
let bottomSeparator = Child(Rectangle.self)
let button = Child(SolidRoundedButtonComponent.self)
let termsText = Child(MultilineTextComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self].value
@ -1007,45 +961,43 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let peer = state.peer, let products = state.products, let selectedProductId = state.selectedProductId {
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumGiftScreenContentComponent(
context: context.component.context,
peer: peer,
products: products,
selectedProductId: selectedProductId,
present: context.component.present,
selectProduct: { [weak state] productId in
state?.selectProduct(id: productId)
}
)),
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { targetContentOffset in
if targetContentOffset.pointee.y < 100.0 {
targetContentOffset.pointee = CGPoint(x: 0.0, y: 0.0)
} else if targetContentOffset.pointee.y < 123.0 {
targetContentOffset.pointee = CGPoint(x: 0.0, y: 123.0)
}
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumGiftScreenContentComponent(
context: context.component.context,
peer: state.peer,
products: state.products,
selectedProductId: state.selectedProductId,
present: context.component.present,
selectProduct: { [weak state] productId in
state?.selectProduct(id: productId)
}
),
environment: { environment },
availableSize: context.availableSize,
transition: context.transition
)
context.add(scrollContent
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
}
)),
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { targetContentOffset in
if targetContentOffset.pointee.y < 100.0 {
targetContentOffset.pointee = CGPoint(x: 0.0, y: 0.0)
} else if targetContentOffset.pointee.y < 123.0 {
targetContentOffset.pointee = CGPoint(x: 0.0, y: 123.0)
}
}
),
environment: { environment },
availableSize: context.availableSize,
transition: context.transition
)
context.add(scrollContent
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let topPanelAlpha: CGFloat
let titleOffset: CGFloat
let titleScale: CGFloat
@ -1066,23 +1018,21 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
titleAlpha = 1.0
}
if let peer = context.state.peer {
let star = star.update(
component: GiftAvatarComponent(
context: context.component.context,
peer: peer,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset + star.size.height / 2.0 - 30.0 - titleOffset * titleScale))
.scale(titleScale)
)
}
let star = star.update(
component: GiftAvatarComponent(
context: context.component.context,
peer: context.state.peer,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset + star.size.height / 2.0 - 30.0 - titleOffset * titleScale))
.scale(titleScale)
)
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
@ -1098,7 +1048,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
.scale(titleScale)
.opacity(titleAlpha)
)
let price: String?
if let products = state.products, let selectedProductId = state.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) {
price = product.price
@ -1193,6 +1143,56 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
})
)
if let _ = context.state.peer {
let accountContext = context.component.context
let present = context.component.present
let sideInset: CGFloat = 16.0
let textSideInset: CGFloat = 16.0
let availableWidth = context.availableSize.width
let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right
let termsFont = Font.regular(13.0)
let termsTextColor = environment.theme.list.freeTextColor
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let termsString: MultilineTextComponent.TextContent = .markdown(
text: environment.strings.Premium_Gift_Info,
attributes: termsMarkdownAttributes
)
let termsText = termsText.update(
component: MultilineTextComponent(
text: termsString,
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
let controller = PremiumIntroScreen(context: accountContext, source: .giftTerms)
present(controller)
}
}
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(termsText
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: context.availableSize.height - bottomPanel.size.height - termsText.size.height))
)
}
return context.availableSize
}
}
@ -1248,24 +1248,16 @@ public final class PremiumGiftScreen: ViewControllerComponentContainer {
strongSelf.view.disablesInteractiveModalDismiss = inProgress
}
}
// presentImpl = { [weak self] c in
// self?.present(c, in: .window(.root))
// }
pushImpl = { [weak self] c in
self?.push(c)
}
completionImpl = { [weak self] duration in
if let strongSelf = self {
let navigationController = strongSelf.navigationController
strongSelf.dismiss()
let introController = PremiumIntroScreen(context: context, source: .gift(from: context.account.peerId, to: peerId, duration: duration))
navigationController?.pushViewController(introController, animated: true)
Queue.mainQueue().after(0.1, {
introController.view.addSubview(ConfettiView(frame: introController.view.bounds))
})
if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is PeerInfoScreen) && !($0 is PremiumGiftScreen) }
navigationController.setViewControllers(controllers, animated: true)
}
}
}

View File

@ -39,6 +39,7 @@ public enum PremiumSource: Equatable {
case deeplink(String?)
case profile(PeerId)
case gift(from: PeerId, to: PeerId, duration: Int32)
case giftTerms
var identifier: String? {
switch self {
@ -74,7 +75,7 @@ public enum PremiumSource: Equatable {
return "double_limits__about"
case let .profile(id):
return "profile__\(id.id._internalGetInt64Value())"
case .gift:
case .gift, .giftTerms:
return nil
case let .deeplink(reference):
if let reference = reference {
@ -819,7 +820,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let boldTextFont = Font.semibold(15.0)
let textString: String
if let _ = context.component.otherPeerName {
if case .giftTerms = context.component.source {
textString = strings.Premium_PersonalDescription
} else if let _ = context.component.otherPeerName {
if case let .gift(fromId, _, _) = context.component.source {
if fromId == context.component.context.account.peerId {
textString = strings.Premium_GiftedDescriptionYou
@ -1447,7 +1450,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
)
let titleString: String
if case .gift = context.component.source {
if case .giftTerms = context.component.source {
titleString = environment.strings.Premium_Title
} else if case .gift = context.component.source {
titleString = environment.strings.Premium_GiftedTitle
} else if state.isPremium == true {
titleString = environment.strings.Premium_SubscribedTitle
@ -1482,21 +1487,21 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
secondaryTitleText = environment.strings.Premium_PersonalTitle(otherPeerName).string
} else if case let .gift(fromPeerId, _, duration) = context.component.source {
if fromPeerId == context.component.context.account.peerId {
if duration >= 86400 * 365 {
if duration == 12 {
secondaryTitleText = environment.strings.Premium_GiftedTitleYou_12Month(otherPeerName).string
} else if duration >= 86400 * 180 {
} else if duration == 6 {
secondaryTitleText = environment.strings.Premium_GiftedTitleYou_6Month(otherPeerName).string
} else if duration >= 86400 * 90 {
} else if duration == 3 {
secondaryTitleText = environment.strings.Premium_GiftedTitleYou_3Month(otherPeerName).string
} else {
secondaryTitleText = ""
}
} else {
if duration >= 86400 * 365 {
if duration == 12 {
secondaryTitleText = environment.strings.Premium_GiftedTitle_12Month(otherPeerName).string
} else if duration >= 86400 * 180 {
} else if duration == 6 {
secondaryTitleText = environment.strings.Premium_GiftedTitle_6Month(otherPeerName).string
} else if duration >= 86400 * 90 {
} else if duration == 3 {
secondaryTitleText = environment.strings.Premium_GiftedTitle_3Month(otherPeerName).string
} else {
secondaryTitleText = ""
@ -1517,7 +1522,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
maximumNumberOfLines: 2,
lineSpacing: 0.0
),
availableSize: context.availableSize,
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.width),
transition: context.transition
)

View File

@ -650,21 +650,23 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
let doneImpl: (Bool) -> Void = { [weak self] shouldDelay in
let minDelay: Double = shouldDelay ? 0.9 : 0.6
let delay: Double
let hapticDelay: Double
if let strongSelf = self, let contentNode = strongSelf.contentNode as? ShareProlongedLoadingContainerNode {
delay = contentNode.completionDuration
if shouldDelay {
Queue.mainQueue().after(delay - 1.5, {
if strongSelf.hapticFeedback == nil {
strongSelf.hapticFeedback = HapticFeedback()
}
strongSelf.hapticFeedback?.success()
})
}
hapticDelay = shouldDelay ? delay - 1.5 : delay
} else {
delay = max(minDelay, (timestamp + minDelay) - CACurrentMediaTime())
hapticDelay = delay
}
Queue.mainQueue().after(hapticDelay, {
if self?.hapticFeedback == nil {
self?.hapticFeedback = HapticFeedback()
}
self?.hapticFeedback?.success()
})
Queue.mainQueue().after(delay, {
self?.animateOut(shared: true, completion: {
self?.dismiss?(true)

View File

@ -1190,8 +1190,21 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
view.expandFromPictureInPicture()
}
if let validLayout = self.validLayout {
self.view.clipsToBounds = true
self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius
if #available(iOS 13.0, *) {
self.view.layer.cornerCurve = .continuous
}
self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.width * 0.9, y: 117.0), to: self.view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
self?.view.layer.cornerRadius = 0.0
})
self.view.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
self.view.layer.allowsGroupOpacity = true
self.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] _ in
self.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
@ -1226,6 +1239,18 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
strongSelf.view.layer.allowsGroupOpacity = false
strongSelf.dismissImpl(completion: completion)
})
if let validLayout = self.validLayout {
self.view.clipsToBounds = true
self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius
if #available(iOS 13.0, *) {
self.view.layer.cornerCurve = .continuous
}
self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.frame.width * 0.9, y: 117.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
})
self.view.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
}
private func dismissImpl(completion: (() -> Void)? = nil) {

View File

@ -39,7 +39,7 @@ public struct Namespaces {
}
public struct ItemCollection {
public static let CloudStickerPacks: Int32 = 7
public static let CloudStickerPacks: Int32 = 0
public static let CloudMaskPacks: Int32 = 1
public static let EmojiKeywords: Int32 = 2
public static let CloudAnimatedEmoji: Int32 = 3

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "gift.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,188 @@
%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 2.335022 2.334961 cm
0.000000 0.000000 0.000000 scn
3.865000 17.330078 m
3.837526 17.330078 l
3.300828 17.330086 2.857980 17.330093 2.497252 17.300621 c
2.122623 17.270012 1.778399 17.204330 1.455116 17.039610 c
0.953664 16.784107 0.545970 16.376415 0.290468 15.874963 c
0.125747 15.551680 0.060066 15.207455 0.029457 14.832827 c
-0.000016 14.472098 -0.000009 14.029249 0.000000 13.492552 c
0.000000 13.465077 l
0.000000 7.865078 l
0.000000 7.837605 l
-0.000009 7.300907 -0.000016 6.858058 0.029457 6.497330 c
0.060066 6.122702 0.125747 5.778477 0.290468 5.455194 c
0.545970 4.953741 0.953664 4.546048 1.455116 4.290545 c
1.778399 4.125825 2.122623 4.060143 2.497252 4.029535 c
2.857977 4.000063 3.300821 4.000070 3.837511 4.000078 c
3.837541 4.000078 l
3.865000 4.000078 l
4.665000 4.000078 l
5.032270 4.000078 5.330000 4.297809 5.330000 4.665078 c
5.330000 5.032348 5.032270 5.330078 4.665000 5.330078 c
3.865000 5.330078 l
3.293975 5.330078 2.905699 5.330595 2.605556 5.355118 c
2.313176 5.379006 2.163463 5.422318 2.058923 5.475584 c
1.807727 5.603576 1.603498 5.807804 1.475507 6.059001 c
1.422241 6.163542 1.378929 6.313254 1.355040 6.605635 c
1.330518 6.905777 1.330000 7.294052 1.330000 7.865078 c
1.330000 13.465077 l
1.330000 14.036103 1.330518 14.424378 1.355040 14.724522 c
1.378929 15.016902 1.422241 15.166615 1.475507 15.271155 c
1.603498 15.522351 1.807727 15.726581 2.058923 15.854571 c
2.163463 15.907838 2.313176 15.951150 2.605556 15.975038 c
2.905699 15.999560 3.293974 16.000078 3.865000 16.000078 c
15.465000 16.000078 l
16.036026 16.000078 16.424301 15.999560 16.724445 15.975038 c
17.016823 15.951150 17.166538 15.907838 17.271076 15.854571 c
17.522274 15.726581 17.726501 15.522351 17.854492 15.271155 c
17.907761 15.166615 17.951073 15.016902 17.974960 14.724522 c
17.999481 14.424378 18.000000 14.036104 18.000000 13.465078 c
18.000000 7.865078 l
18.000000 7.294053 17.999481 6.905778 17.974960 6.605635 c
17.951073 6.313254 17.907761 6.163542 17.854492 6.059001 c
17.726501 5.807804 17.522274 5.603576 17.271076 5.475584 c
17.166538 5.422318 17.016823 5.379006 16.724445 5.355118 c
16.424301 5.330595 16.036026 5.330078 15.465000 5.330078 c
14.665000 5.330078 l
14.297730 5.330078 14.000000 5.032348 14.000000 4.665078 c
14.000000 4.297809 14.297730 4.000078 14.665000 4.000078 c
15.465000 4.000078 l
15.492459 4.000078 l
15.492488 4.000078 l
16.029179 4.000070 16.472023 4.000063 16.832748 4.029535 c
17.207378 4.060143 17.551601 4.125825 17.874886 4.290545 c
18.376335 4.546048 18.784031 4.953741 19.039532 5.455194 c
19.204254 5.778477 19.269936 6.122702 19.300545 6.497330 c
19.330015 6.858045 19.330009 7.300875 19.330002 7.837547 c
19.330002 7.837619 l
19.330002 7.865078 l
19.330002 13.465078 l
19.330002 13.492537 l
19.330002 13.492610 l
19.330009 14.029282 19.330015 14.472111 19.300545 14.832827 c
19.269936 15.207455 19.204254 15.551680 19.039532 15.874963 c
18.784031 16.376415 18.376335 16.784107 17.874886 17.039610 c
17.551601 17.204330 17.207378 17.270012 16.832748 17.300621 c
16.472019 17.330093 16.029171 17.330086 15.492474 17.330078 c
15.465000 17.330078 l
3.865000 17.330078 l
h
9.549997 5.417668 m
9.624264 5.444814 9.705738 5.444814 9.780005 5.417668 c
9.779352 5.417907 9.779119 5.418036 9.779340 5.417960 c
9.779449 5.417922 9.779672 5.417834 9.780010 5.417685 c
9.785703 5.415166 9.824282 5.395164 9.915651 5.303084 c
10.027624 5.190243 10.164604 5.026791 10.383287 4.764371 c
11.967221 2.863649 l
12.324518 2.434894 12.562548 2.148232 12.716402 1.925475 c
12.838650 1.748481 12.861221 1.674737 12.864748 1.663215 c
12.864949 1.662560 l
12.864219 1.564913 12.820898 1.472416 12.746351 1.409344 c
12.745722 1.409081 l
12.734627 1.404418 12.663531 1.374542 12.449274 1.355145 c
12.179653 1.330734 11.807049 1.330078 11.248934 1.330078 c
8.081068 1.330078 l
7.522953 1.330078 7.150349 1.330734 6.880728 1.355145 c
6.666473 1.374543 6.595378 1.404416 6.584280 1.409080 c
6.583652 1.409343 l
6.509105 1.472416 6.465783 1.564913 6.465053 1.662560 c
6.465254 1.663213 l
6.468780 1.674733 6.491349 1.748475 6.613600 1.925474 c
6.767454 2.148230 7.005483 2.434893 7.362779 2.863647 c
8.946714 4.764370 l
9.165398 5.026790 9.302377 5.190243 9.414351 5.303084 c
9.505718 5.395163 9.544298 5.415166 9.549992 5.417685 c
9.551012 5.418137 9.550975 5.418026 9.549997 5.417668 c
h
10.236588 6.666842 m
9.867472 6.801756 9.462530 6.801756 9.093414 6.666842 c
8.834289 6.572128 8.636753 6.407670 8.470269 6.239893 c
8.312561 6.080961 8.138463 5.872022 7.942029 5.636275 c
7.924980 5.615815 l
6.341045 3.715093 l
6.320368 3.690281 l
5.989172 3.292866 5.711553 2.959741 5.519254 2.681324 c
5.330824 2.408508 5.141049 2.075929 5.135232 1.690181 c
5.127517 1.178505 5.355523 0.691704 5.753542 0.370064 c
6.053606 0.127583 6.430592 0.060459 6.760806 0.030563 c
7.097775 0.000055 7.531381 0.000065 8.048665 0.000076 c
8.048759 0.000076 l
8.081068 0.000076 l
11.248934 0.000076 l
11.281242 0.000076 l
11.281337 0.000076 l
11.798622 0.000065 12.232226 0.000055 12.569197 0.030563 c
12.899410 0.060459 13.276397 0.127583 13.576460 0.370064 c
13.974480 0.691704 14.202486 1.178505 14.194770 1.690181 c
14.188953 2.075929 13.999178 2.408508 13.810747 2.681325 c
13.618445 2.959745 13.340822 3.292877 13.009618 3.690300 c
12.988955 3.715096 l
11.405022 5.615816 l
11.387974 5.636274 l
11.191540 5.872021 11.017441 6.080961 10.859733 6.239894 c
10.693249 6.407670 10.495712 6.572128 10.236588 6.666842 c
h
f*
n
Q
endstream
endobj
3 0 obj
5505
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.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
0000005595 00000 n
0000005618 00000 n
0000005791 00000 n
0000005865 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5924
%%EOF

View File

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

View File

@ -3139,15 +3139,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self?.displayPsa(type: type, sourceNode: sourceNode, isAutomatic: false)
}, displayDiceTooltip: { [weak self] dice in
self?.displayDiceTooltip(dice: dice)
}, animateDiceSuccess: { [weak self] onlyHaptic in
}, animateDiceSuccess: { [weak self] haptic, confetti in
guard let strongSelf = self else {
return
}
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
if !onlyHaptic {
if haptic {
strongSelf.selectPollOptionFeedback?.success()
}
if confetti {
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
}
}, displayPremiumStickerTooltip: { [weak self] file, message in

View File

@ -122,7 +122,7 @@ public final class ChatControllerInteraction {
let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void
let displayPsa: (String, ASDisplayNode) -> Void
let displayDiceTooltip: (TelegramMediaDice) -> Void
let animateDiceSuccess: (Bool) -> Void
let animateDiceSuccess: (Bool, Bool) -> Void
let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void
let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openMessageReplies: (MessageId, Bool, Bool) -> Void
@ -225,7 +225,7 @@ public final class ChatControllerInteraction {
displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void,
displayPsa: @escaping (String, ASDisplayNode) -> Void,
displayDiceTooltip: @escaping (TelegramMediaDice) -> Void,
animateDiceSuccess: @escaping (Bool) -> Void,
animateDiceSuccess: @escaping (Bool, Bool) -> Void,
displayPremiumStickerTooltip: @escaping (TelegramMediaFile, Message) -> Void,
openPeerContextMenu: @escaping (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void,
openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void,

View File

@ -423,7 +423,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if !item.message.effectivelyIncoming(item.context.account.peerId) {
animationNode.success = { [weak self] onlyHaptic in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.animateDiceSuccess(onlyHaptic)
item.controllerInteraction.animateDiceSuccess(true, !onlyHaptic)
}
}
}
@ -433,7 +433,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if !item.message.effectivelyIncoming(item.context.account.peerId) {
animationNode.success = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.animateDiceSuccess(false)
item.controllerInteraction.animateDiceSuccess(true, true)
}
}
}
@ -1695,7 +1695,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
.offsetBy(dx: incomingMessage ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0)
animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0))
}
animationFrame = animationFrame.offsetBy(dx: 0.0, dy: self.insets.top)
additionalAnimationNode.frame = animationFrame
if incomingMessage {
additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)

View File

@ -14,6 +14,8 @@ import UrlEscaping
import TelegramStringFormatting
import WallpaperBackgroundNode
import ReactionSelectionNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false)
@ -29,8 +31,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let mediaBackgroundNode: NavigationBackgroundNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private let giftNode: ASImageNode
private let animationNode: AnimatedStickerNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonStarsNode: PremiumStarsNode
@ -39,6 +40,29 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
private var absoluteRect: (CGRect, CGSize)?
private var isPlaying: Bool = false
private var wasPending: Bool = false
private var didChangeFromPendingToSent: Bool = false
override var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool? {
didSet {
if self.visibilityStatus != oldValue {
self.updateVisibility()
}
}
}
required init() {
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
@ -62,10 +86,8 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
self.giftNode = ASImageNode()
self.giftNode.isUserInteractionEnabled = false
self.giftNode.displaysAsynchronously = false
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.buttonStarsNode = PremiumStarsNode()
@ -80,7 +102,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.giftNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.buttonStarsNode)
@ -128,18 +150,29 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let imageSize = CGSize(width: 220.0, height: 210.0)
let giftSize = CGSize(width: 220.0, height: 240.0)
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId)
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
var duration: String = ""
var animationName: String = ""
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case let .giftPremium(_, _, durationValue):
duration = item.presentationData.strings.Notification_PremiumGift_Subtitle(timeIntervalString(strings: item.presentationData.strings, value: durationValue)).string
case let .giftPremium(_, _, months):
duration = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
switch months {
case 12:
animationName = "Gift2"
case 6:
animationName = "Gift1"
case 3:
animationName = "Gift3"
default:
animationName = "Gift3"
}
default:
break
}
@ -148,11 +181,11 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_Title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: imageSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_Title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: duration, font: Font.regular(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: imageSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: duration, font: Font.regular(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: imageSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var labelRects = labelLayout.linesRects()
if labelRects.count > 1 {
@ -184,16 +217,28 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
backgroundMaskUpdated = true
}
let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + imageSize.height + 18.0)
let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + giftSize.height + 18.0)
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.autoplay = true
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
}
strongSelf.item = item
if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal {
strongSelf.wasPending = true
}
if strongSelf.wasPending && (item.message.id.namespace != Namespaces.Message.Local && item.message.id.namespace != Namespaces.Message.ScheduledLocal) {
strongSelf.didChangeFromPendingToSent = true
}
strongSelf.updateVisibility()
strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: labelLayout.size.height + 16.0), size: imageSize)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: labelLayout.size.height + 16.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
@ -201,10 +246,9 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
strongSelf.giftNode.image = UIImage(bundleImageName: "Components/Gift")
if let image = strongSelf.giftNode.image {
strongSelf.giftNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - image.size.width) / 2.0), y: mediaBackgroundFrame.minY + 14.0), size: image.size)
}
let iconSize = CGSize(width: 160.0, height: 160.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = labelApply()
let _ = titleApply()
@ -214,7 +258,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 121.0), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY - 1.0), size: subtitleLayout.size)
@ -371,4 +415,37 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return .none
}
}
private func updateVisibility() {
guard let item = self.item else {
return
}
let isPlaying = self.visibilityStatus == true
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
self.animationNode.visibility = isPlaying
}
if isPlaying {
var alreadySeen = true
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
if unreadRange.contains(item.message.id.id) {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = false
}
}
}
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
self.animationNode.playOnce()
}
if !alreadySeen {
item.controllerInteraction.animateDiceSuccess(false, true)
}
}
}
}

View File

@ -517,7 +517,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in

View File

@ -143,7 +143,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in

View File

@ -11,6 +11,7 @@ import ChatListUI
import PeerAvatarGalleryUI
import SettingsUI
import ChatPresentationInterfaceState
import AttachmentUI
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
var found = false
@ -21,6 +22,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
if let updateTextInputState = params.updateTextInputState {
controller.updateTextInputState(updateTextInputState)
}
var popAndComplete = true
if let subject = params.subject, case let .message(messageSubject, _, timecode) = subject {
if case let .id(messageId) = messageSubject {
let navigationController = params.navigationController
@ -33,20 +35,21 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
(navigationController?.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
})
}
popAndComplete = false
} else if params.scrollToEndIfExists && isFirst {
controller.scrollToEndOfHistory()
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller)
} else if let search = params.activateMessageSearch {
controller.activateSearch(domain: search.0, query: search.1)
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller)
} else if let reportReason = params.reportReason {
controller.beginReportSelection(reason: reportReason)
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller)
} else {
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
}
if popAndComplete {
if let _ = params.navigationController.viewControllers.last as? AttachmentController, let controller = params.navigationController.viewControllers[params.navigationController.viewControllers.count - 2] as? ChatControllerImpl, controller.chatLocation == params.chatLocation {
} else {
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
}
params.completion(controller)
}
controller.purposefulAction = params.purposefulAction

View File

@ -135,7 +135,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in

View File

@ -2336,7 +2336,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in

View File

@ -1313,7 +1313,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in

View File

@ -0,0 +1,121 @@
import Foundation
import AVFoundation
import SwiftSignalKit
import UniversalMediaPlayer
import AccountContext
import AVKit
public class ExternalVideoPlayer: NSObject, AVRoutePickerViewDelegate {
private let context: AccountContext
let content: NativeVideoContent
let player: AVPlayer?
private var didPlayToEndTimeObserver: NSObjectProtocol?
private var timeObserver: Any?
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: false, progress: 0.0, display: true), soundEnabled: true)
private let _status = ValuePromise<MediaPlayerStatus>()
var status: Signal<MediaPlayerStatus, NoError> {
return self._status.get()
}
private var seekId: Int = 0
private weak var routePickerView: UIView?
public var isActiveUpdated: (Bool) -> Void = { _ in }
public init(context: AccountContext, content: NativeVideoContent) {
self.context = context
self.content = content
if let path = context.account.postbox.mediaBox.completedResourcePath(content.fileReference.media.resource, pathExtension: "mp4") {
let player = AVPlayer(url: URL(fileURLWithPath: path))
self.player = player
} else {
self.player = nil
}
super.init()
self.startObservingForAirPlayStatusChanges()
self.isActiveUpdated(self.player?.isExternalPlaybackActive ?? false)
if let player = self.player {
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
if let strongSelf = self {
strongSelf.player?.seek(to: CMTime(seconds: 0.0, preferredTimescale: 30))
strongSelf.play()
}
})
self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in
guard let strongSelf = self else {
return
}
strongSelf.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: strongSelf.statusValue.duration, dimensions: CGSize(), timestamp: CMTimeGetSeconds(time), baseRate: 1.0, seekId: strongSelf.seekId, status: strongSelf.statusValue.status, soundEnabled: true)
strongSelf._status.set(strongSelf.statusValue)
}
}
self._status.set(self.statusValue)
}
deinit {
if let timeObserver = self.timeObserver {
self.player?.removeTimeObserver(timeObserver)
}
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
}
self.stopObservingForAirPlayStatusChanges()
}
public func play() {
self.player?.play()
}
public func openRouteSelection() {
if #available(iOS 11.0, *) {
let routePickerView = AVRoutePickerView()
routePickerView.delegate = self
if #available(iOS 13.0, *) {
routePickerView.prioritizesVideoDevices = true
}
self.context.sharedContext.mainWindow?.viewController?.view.addSubview(routePickerView)
if let routePickerButton = routePickerView.subviews.first(where: { $0 is UIButton }) as? UIButton {
routePickerButton.sendActions(for: .touchUpInside)
}
}
}
@available(iOS 11.0, *)
public func routePickerViewDidEndPresentingRoutes(_ routePickerView: AVRoutePickerView) {
routePickerView.removeFromSuperview()
self.play()
}
private var observerContextAirplay = 1
func startObservingForAirPlayStatusChanges()
{
self.player?.addObserver(self, forKeyPath: #keyPath(AVPlayer.isExternalPlaybackActive), options: .new, context: &observerContextAirplay)
}
func stopObservingForAirPlayStatusChanges()
{
self.player?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.isExternalPlaybackActive))
}
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &observerContextAirplay {
self.isActiveUpdated(self.player?.isExternalPlaybackActive ?? false)
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

View File

@ -880,17 +880,17 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal:
if self.textNode.tapAttributeAction != nil {
if self.textNode.tapAttributeAction != nil || displayUndo {
self.isUserInteractionEnabled = true
} else {
self.isUserInteractionEnabled = false
}
case let .sticker(_, _, _, _, undoText, _):
self.isUserInteractionEnabled = undoText != nil
case .sticker:
self.isUserInteractionEnabled = displayUndo
case .dice:
self.panelWrapperNode.clipsToBounds = true
case .info:
if self.textNode.tapAttributeAction != nil {
if self.textNode.tapAttributeAction != nil || displayUndo {
self.isUserInteractionEnabled = true
} else {
self.isUserInteractionEnabled = false

View File

@ -723,11 +723,71 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.headerColorKey = colorKey
self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear))
}
case "web_app_open_popup":
if let json = json, let message = json["message"] as? String, let buttons = json["buttons"] as? [Any] {
let presentationData = self.presentationData
let title = json["title"] as? String
var alertButtons: [TextAlertAction] = []
for buttonJson in buttons {
if let button = buttonJson as? [String: Any], let id = button["id"] as? String, let type = button["type"] as? String {
let buttonAction = {
self.sendAlertButtonEvent(id: id)
}
let text = button["text"] as? String
switch type {
case "default":
if let text = text {
alertButtons.append(TextAlertAction(type: .genericAction, title: text, action: {
buttonAction()
}))
}
case "destructive":
if let text = text {
alertButtons.append(TextAlertAction(type: .destructiveAction, title: text, action: {
buttonAction()
}))
}
case "ok":
alertButtons.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
buttonAction()
}))
case "cancel":
alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
buttonAction()
}))
case "close":
alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {
buttonAction()
}))
default:
break
}
}
}
var actionLayout: TextAlertContentActionLayout = .horizontal
if alertButtons.count > 2 {
actionLayout = .vertical
}
let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: title, text: message, actions: alertButtons, actionLayout: actionLayout)
alertController.dismissed = {
self.sendAlertButtonEvent(id: nil)
}
self.controller?.present(alertController, in: .window(.root))
}
case "web_app_setup_closing_behavior":
if let json = json, let needConfirmation = json["need_confirmation"] as? Bool {
self.needDismissConfirmation = needConfirmation
}
default:
break
}
}
fileprivate var needDismissConfirmation = false
private var headerColorKey: String?
private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) {
let color: UIColor?
@ -834,6 +894,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
fileprivate func sendSettingsButtonEvent() {
self.webView?.sendEvent(name: "settings_button_pressed", data: nil)
}
fileprivate func sendAlertButtonEvent(id: String?) {
var paramsString: String?
if let id = id {
paramsString = "{button_id: \"\(id)\"}"
}
self.webView?.sendEvent(name: "popup_closed", data: paramsString)
}
}
fileprivate var controllerNode: Node {
@ -945,7 +1013,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
if case .back = self.cancelButtonNode.state {
self.controllerNode.sendBackButtonEvent()
} else {
self.dismiss()
self.requestDismiss {
self.dismiss()
}
}
}
@ -1055,6 +1125,38 @@ public final class WebAppController: ViewController, AttachmentContainable {
public func prepareForReuse() {
self.updateTabBarAlpha(1.0, .immediate)
}
public func requestDismiss(completion: @escaping () -> Void) {
if self.controllerNode.needDismissConfirmation {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: self.presentationData.strings.WebApp_CloseConfirmation),
ActionSheetButtonItem(title: self.presentationData.strings.WebApp_CloseAnyway, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
completion()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.present(actionSheet, in: .window(.root))
} else {
completion()
}
}
public func shouldDismissImmediately() -> Bool {
if self.controllerNode.needDismissConfirmation {
return false
} else {
return true
}
}
}
final class WebAppPickerContext: AttachmentMediaPickerContext {

View File

@ -1,5 +1,5 @@
{
"app": "8.8.2",
"app": "8.8.3",
"bazel": "5.1.0",
"xcode": "13.4.1"
}