mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit 'aa46e81a4928a22f58e9b6240b31104c01767c0a'
# Conflicts: # Telegram/Telegram-iOS/en.lproj/Localizable.strings # submodules/UndoUI/Sources/UndoOverlayControllerNode.swift
This commit is contained in:
commit
815cb9b341
BIN
Telegram/Telegram-iOS/Resources/Gift1.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Gift1.tgs
Normal file
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/Gift2.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Gift2.tgs
Normal file
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/Gift3.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Gift3.tgs
Normal file
Binary file not shown.
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 |
188
submodules/TelegramUI/Images.xcassets/Media Gallery/AirPlay.imageset/Airplay.pdf
vendored
Normal file
188
submodules/TelegramUI/Images.xcassets/Media Gallery/AirPlay.imageset/Airplay.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Media Gallery/AirPlay.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/AirPlay.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Airplay.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -517,7 +517,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
}, animateDiceSuccess: { _ in
|
||||
}, animateDiceSuccess: { _, _ in
|
||||
}, displayPremiumStickerTooltip: { _, _ in
|
||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "8.8.2",
|
||||
"app": "8.8.3",
|
||||
"bazel": "5.1.0",
|
||||
"xcode": "13.4.1"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user