diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e2382215ed..7dc020ede1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9911,3 +9911,5 @@ Sorry for the inconvenience."; "Story.Editor.TooltipReachedReactionLimitTitle" = "Limit Reached"; "Story.Editor.TooltipReachedReactionLimitText" = "You can't add up more than %@ to a story."; +"Gallery.ViewOncePhotoTooltip" = "This photo can only be viewed once."; +"Gallery.ViewOnceVideoTooltip" = "This video can only be viewed once."; diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index bd3b05ce38..893d4ce88c 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -242,7 +242,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendSilently, icon: .sendWithoutSound, hasSeparator: true, action: { sendSilently?() })) - if canSendWhenOnline { + if canSendWhenOnline && schedule != nil { contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: .sendWhenOnline, hasSeparator: true, action: { sendWhenOnline?() })) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index f0ccd4f1d1..ba09050bf8 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -806,6 +806,12 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { switch gestureRecognizer.state { case .began: + self.tapGestureRecognizer?.isEnabled = false + self.tapGestureRecognizer?.isEnabled = true + + self.longPressGestureRecognizer?.isEnabled = false + self.longPressGestureRecognizer?.isEnabled = true + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) let _ = entityView.dismissReactionSelection() diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 6443dcc251..84ce4005a6 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", "//submodules/TooltipUI", + "//submodules/TelegramNotices", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 76ac072cb1..315474fa4b 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2771,6 +2771,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.playbackRatePromise.set(self.playbackRate ?? 1.0) } + public func seekToStart() { + self.videoNode?.seek(0.0) + self.videoNode?.play() + } + override var keyShortcuts: [KeyShortcut] { let strings = self.presentationData.strings diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 076c6ad06c..38be8215c6 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -12,6 +12,7 @@ import ScreenCaptureDetection import AppBundle import LocalizedPeerData import TooltipUI +import TelegramNotices private func galleryMediaForMedia(media: Media) -> Media? { if let media = media as? TelegramMediaImage { @@ -55,7 +56,7 @@ private func mediaForMessage(message: Message) -> Media? { } private final class SecretMediaPreviewControllerNode: GalleryControllerNode { - private var timeoutNode: RadialStatusNode? + fileprivate var timeoutNode: RadialStatusNode? private var validLayout: (ContainerViewLayout, CGFloat)? @@ -68,30 +69,10 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { self.timeoutNode = timeoutNode let icon: RadialStatusNodeState.SecretTimeoutIcon let timeoutValue = Int32(timeout) - if timeoutValue == viewOnceTimeout || "".isEmpty { + if timeoutValue == viewOnceTimeout { beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - if let image = generateImage(CGSize(width: 28.0, height: 28.0), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - - let string = "1" - let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: Font.with(size: 14.0, design: .round), NSAttributedString.Key.foregroundColor: UIColor.white]) - - let line = CTLineCreateWithAttributedString(attributedString) - let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - - let lineOffset = CGPoint(x: -1.0, y: 0.0) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0)) - - context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - - context.translateBy(x: lineOrigin.x, y: lineOrigin.y) - CTLineDraw(line, context) - context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) - }) { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) { icon = .image(image) } else { icon = .flame @@ -104,9 +85,6 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside) -// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.statusTapGesture)) -// timeoutNode.view.addGestureRecognizer(tapGesture) - if let (layout, navigationHeight) = self.validLayout { self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate) } @@ -181,6 +159,9 @@ public final class SecretMediaPreviewController: ViewController { private var currentNodeMessageIsViewOnce = false private var tempFile: TempBoxFile? + private let centralItemAttributesDisposable = DisposableSet(); + private let footerContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() + private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) private var hiddenMediaManagerIndex: Int? @@ -219,6 +200,15 @@ public final class SecretMediaPreviewController: ViewController { return nil } }) + + self.centralItemAttributesDisposable.add(self.footerContentNode.get().start(next: { [weak self] footerContentNode, _ in + guard let self else { + return + } + self.controllerNode.updatePresentationState({ + $0.withUpdatedFooterContentNode(footerContentNode) + }, transition: .immediate) + })) } required public init(coder aDecoder: NSCoder) { @@ -235,6 +225,7 @@ public final class SecretMediaPreviewController: ViewController { if let tempFile = self.tempFile { TempBox.shared.dispose(tempFile) } + self.centralItemAttributesDisposable.dispose() } @objc func donePressed() { @@ -255,9 +246,9 @@ public final class SecretMediaPreviewController: ViewController { self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction) self.displayNodeDidLoad() - self.controllerNode.statusPressed = { [weak self] sourceView in + self.controllerNode.statusPressed = { [weak self] _ in if let self { - self.presentViewOnceTooltip(sourceView: sourceView) + self.presentViewOnceTooltip() } } @@ -279,6 +270,11 @@ public final class SecretMediaPreviewController: ViewController { self.controllerNode.dismiss = { [weak self] in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) + + if let tooltipController = self?.tooltipController { + self?.tooltipController = nil + tooltipController.dismiss() + } } self.controllerNode.beginCustomDismiss = { [weak self] _ in @@ -314,7 +310,7 @@ public final class SecretMediaPreviewController: ViewController { strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -324,7 +320,7 @@ public final class SecretMediaPreviewController: ViewController { strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -354,7 +350,11 @@ public final class SecretMediaPreviewController: ViewController { strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout } - if !message.flags.contains(.Incoming) { + if strongSelf.currentNodeMessageIsVideo { + if let node = strongSelf.controllerNode.pager.centralItemNode() { + strongSelf.footerContentNode.set(node.footerContent()) + } + } else if !message.flags.contains(.Incoming) { if let _ = beginTimeAndTimeout { strongSelf.controllerNode.updatePresentationState({ $0.withUpdatedFooterContentNode(nil) @@ -433,9 +433,26 @@ public final class SecretMediaPreviewController: ViewController { self.controllerNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false) } } + + if self.currentNodeMessageIsViewOnce { + let _ = (ApplicationSpecificNotice.incrementViewOnceTooltip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + guard let self else { + return + } + if count < 2 { + self.presentViewOnceTooltip() + } + }) + } } private func dismiss(forceAway: Bool) { + if let tooltipController = self.tooltipController { + self.tooltipController = nil + tooltipController.dismiss() + } + var animatedOutNode = true var animatedOutInterface = false @@ -490,7 +507,15 @@ public final class SecretMediaPreviewController: ViewController { } guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in - self?.dismiss(forceAway: false) + if let self { + if self.currentNodeMessageIsViewOnce { + if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode { + node.seekToStart() + } + } else { + self.dismiss(forceAway: false) + } + } }, present: { _, _ in }) else { self._ready.set(.single(true)) return @@ -512,7 +537,7 @@ public final class SecretMediaPreviewController: ViewController { } if let attribute = message.autoclearAttribute { if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -520,7 +545,7 @@ public final class SecretMediaPreviewController: ViewController { } } else if let attribute = message.autoremoveAttribute { if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -544,7 +569,11 @@ public final class SecretMediaPreviewController: ViewController { } } - private func presentViewOnceTooltip(sourceView: UIView) { + private func presentViewOnceTooltip() { + guard self.currentNodeMessageIsViewOnce, let sourceView = self.controllerNode.timeoutNode?.view else { + return + } + if let tooltipController = self.tooltipController { self.tooltipController = nil tooltipController.dismiss() @@ -553,12 +582,13 @@ public final class SecretMediaPreviewController: ViewController { let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize()) + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let iconName = "anim_autoremove_on" let text: String if self.currentNodeMessageIsVideo { - text = "This video can only be viewed once" + text = presentationData.strings.Gallery_ViewOnceVideoTooltip } else { - text = "This photo can only be viewed once" + text = presentationData.strings.Gallery_ViewOncePhotoTooltip } let tooltipController = TooltipScreen( @@ -566,6 +596,7 @@ public final class SecretMediaPreviewController: ViewController { sharedContext: self.context.sharedContext, text: .plain(text: text), balancedTextLayout: true, + constrainWidth: 210.0, style: .customBlur(UIColor(rgb: 0x18181a), 0.0), arrowStyle: .small, icon: .animation(name: iconName, delay: 0.1, tintColor: nil), diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index 182b89cfd7..6cdb334208 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1568,7 +1568,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus } } - TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSendWhenOnline:false canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder hasTimer:strongSelf->_hasTimer]; + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSendWhenOnline:effectiveHasSchedule canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder hasTimer:strongSelf->_hasTimer]; controller.send = ^{ __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index b5103bb1f8..c7080a2b7e 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -165,7 +165,7 @@ } } - TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:true canSchedule:effectiveHasSchedule reminder:reminder hasTimer:hasTimer]; + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:effectiveHasSchedule canSchedule:effectiveHasSchedule reminder:reminder hasTimer:false]; controller.send = ^{ __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil) diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 0850de8c80..7604afc788 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -229,6 +229,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, var effectiveHasSchedule = hasSchedule if let editingContext = editingContext { + if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 { + effectiveHasSchedule = false + } for item in selectionContext.selectedItems() { if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 { effectiveHasSchedule = false @@ -239,6 +242,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, let sendWhenOnlineAvailable: Signal if let peer { + if case .secretChat = peer { + effectiveHasSchedule = false + } sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id) |> take(1) |> map { peerView -> Bool in @@ -265,7 +271,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, |> take(1) |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) - let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) + let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false) let dismissImpl = { [weak model] in model?.dismiss(true, false) dismissAll() diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index 500eba5206..bd2ec3b6ff 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -133,7 +133,11 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - self.progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout)) + var progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout)) + if self.timeout == 0x7fffffff { + progress = 0.0 + } + self.progress = progress if self.sparks { let lineWidth: CGFloat = 1.75 @@ -202,6 +206,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters { + var drawArc = true if case let .image(icon) = parameters.icon, let iconImage = icon.cgImage { let imageRect = CGRect(origin: CGPoint(x: floor((bounds.size.width - icon.size.width) / 2.0), y: floor((bounds.size.height - icon.size.height) / 2.0)), size: icon.size) context.saveGState() @@ -210,6 +215,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { context.translateBy(x: -imageRect.midX, y: -imageRect.midY) context.draw(iconImage, in: imageRect) context.restoreGState() + + drawArc = false } let lineWidth: CGFloat @@ -232,10 +239,12 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { let startAngle: CGFloat = -CGFloat.pi / 2.0 let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress - let path = CGMutablePath() - path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) - context.addPath(path) - context.strokePath() + if drawArc { + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + } for particle in parameters.particles { let size: CGFloat = 1.3 diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 97735c297f..3063bc9cad 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -9,7 +9,7 @@ import Tuples import ImageBlur import FastBlur -private func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { +public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in var colorSize: Int32 = 0 memcpy(&colorSize, bytes.baseAddress, 4) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index b0931e94c7..9ddebaba24 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 163 + return 164 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index be331d25db..19112f742b 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -179,6 +179,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case displayChatListArchiveTooltip = 45 case displayStoryReactionTooltip = 46 case storyStealthModeReplyCount = 47 + case viewOnceTooltip = 48 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -424,6 +425,10 @@ private struct ApplicationSpecificNoticeKeys { static func storyStealthModeReplyCount() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.storyStealthModeReplyCount.key) } + + static func viewOnceTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.viewOnceTooltip.key) + } } public struct ApplicationSpecificNotice { @@ -1611,4 +1616,21 @@ public struct ApplicationSpecificNotice { } |> ignoreValues } + + public static func incrementViewOnceTooltip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.viewOnceTooltip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.viewOnceTooltip(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 77f6f18dca..7b0736df4a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -133,10 +133,7 @@ public enum PresentationResourceKey: Int32 { case chatMediaConsumableContentIcon case chatBubbleMediaOverlayControlSecret - - case chatBubbleSecretMediaIcon - case chatBubbleSecretMediaCompactIcon - + case chatInstantVideoWithWallpaperBackgroundImage case chatInstantVideoWithoutWallpaperBackgroundImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index c26414e8e6..9e38d8bdd6 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -164,27 +164,7 @@ public struct PresentationResourcesChat { return generateFilledCircleImage(diameter: 4.0, color: theme.chat.message.mediaDateAndStatusTextColor) }) } - - public static func chatBubbleSecretMediaIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleSecretMediaIcon.rawValue, { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.message.mediaOverlayControlColors.foregroundColor) - }) - } - - public static func chatBubbleSecretMediaCompactIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleSecretMediaCompactIcon.rawValue, { theme in - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.message.mediaOverlayControlColors.foregroundColor) { - let factor: CGFloat = 0.6 - return generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) - }) - } else { - return nil - } - }) - } - + public static func chatInstantVideoBackgroundImage(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? { let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatInstantVideoWithoutWallpaperBackgroundImage : PresentationResourceKey.chatInstantVideoWithWallpaperBackgroundImage return theme.image(key.rawValue, { theme in diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 6f7b9677f9..1416d25a2a 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -547,7 +547,9 @@ public final class EmojiTextAttachmentView: UIView { public var isActive: Bool = true { didSet { - + if self.isActive != oldValue { + self.contentLayer.isVisibleForAnimations = self.isActive + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 70f7a61c84..11f8e2c312 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -137,6 +137,7 @@ public final class MediaEditor { return position } } + public var duration: Double? { if let _ = self.player { if let trimRange = self.values.videoTrimRange { @@ -149,6 +150,14 @@ public final class MediaEditor { } } + public var originalDuration: Double? { + if let _ = self.player { + return min(60.0, self.playerPlaybackState.0) + } else { + return nil + } + } + public var onFirstDisplay: () -> Void = {} public func playerState(framesCount: Int) -> Signal { @@ -340,12 +349,21 @@ public final class MediaEditor { deinit { self.textureSourceDisposable?.dispose() - + self.destroyTimeObservers() + } + + private func destroyTimeObservers() { if let timeObserver = self.timeObserver { - self.player?.removeTimeObserver(timeObserver) + if self.sourceIsVideo { + self.player?.removeTimeObserver(timeObserver) + } else { + self.audioPlayer?.removeTimeObserver(timeObserver) + } + self.timeObserver = nil } if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + self.didPlayToEndTimeObserver = nil } self.audioDelayTimer?.invalidate() @@ -883,7 +901,16 @@ public final class MediaEditor { if self.player == nil, let audioPlayer = self.audioPlayer { let itemTime = audioPlayer.currentItem?.currentTime() ?? .invalid - audioPlayer.setRate(rate, time: itemTime, atHostTime: futureTime) + if audioPlayer.status == .readyToPlay { + audioPlayer.setRate(rate, time: itemTime, atHostTime: futureTime) + } else { + audioPlayer.seek(to: itemTime, toleranceBefore: .zero, toleranceAfter: .zero) + if rate > 0.0 { + audioPlayer.play() + } else { + audioPlayer.pause() + } + } } else { let itemTime = self.player?.currentItem?.currentTime() ?? .invalid let audioTime = self.audioTime(for: itemTime) @@ -891,13 +918,24 @@ public final class MediaEditor { self.player?.setRate(rate, time: itemTime, atHostTime: futureTime) self.additionalPlayer?.setRate(rate, time: itemTime, atHostTime: futureTime) - if rate > 0.0, let audioDelay = self.audioDelay(for: itemTime) { - self.audioDelayTimer = SwiftSignalKit.Timer(timeout: audioDelay, repeat: false, completion: { [weak self] in - self?.audioPlayer?.setRate(rate, time: audioTime, atHostTime: futureTime) - }, queue: Queue.mainQueue()) - self.audioDelayTimer?.start() - } else { - self.audioPlayer?.setRate(rate, time: audioTime, atHostTime: futureTime) + if let audioPlayer = self.audioPlayer { + if rate > 0.0, let audioDelay = self.audioDelay(for: itemTime) { + self.audioDelayTimer = SwiftSignalKit.Timer(timeout: audioDelay, repeat: false, completion: { [weak self] in + self?.audioPlayer?.setRate(rate, time: audioTime, atHostTime: futureTime) + }, queue: Queue.mainQueue()) + self.audioDelayTimer?.start() + } else { + if audioPlayer.status == .readyToPlay { + audioPlayer.setRate(rate, time: audioTime, atHostTime: futureTime) + } else { + audioPlayer.seek(to: audioTime, toleranceBefore: .zero, toleranceAfter: .zero) + if rate > 0.0 { + audioPlayer.play() + } else { + audioPlayer.pause() + } + } + } } } @@ -1017,11 +1055,11 @@ public final class MediaEditor { } } else if let audioPlayer = self.audioPlayer { audioPlayer.pause() + + self.destroyTimeObservers() + self.audioPlayer = nil - - self.audioDelayTimer?.invalidate() - self.audioDelayTimer = nil - + if !self.sourceIsVideo { self.playerPromise.set(.single(nil)) } @@ -1047,10 +1085,16 @@ public final class MediaEditor { if apply { let offset = offset ?? 0.0 let duration = self.duration ?? 0.0 + let lowerBound = self.values.audioTrackTrimRange?.lowerBound ?? 0.0 let upperBound = self.values.audioTrackTrimRange?.upperBound ?? duration - let time = self.player?.currentTime() ?? .zero - let audioTime = self.audioTime(for: time) + let audioTime: CMTime + if self.sourceIsVideo { + let time = self.player?.currentTime() ?? .zero + audioTime = self.audioTime(for: time) + } else { + audioTime = CMTime(seconds: offset + lowerBound, preferredTimescale: CMTimeScale(1000)) + } self.audioPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: offset + upperBound, preferredTimescale: CMTimeScale(1000)) self.audioPlayer?.seek(to: audioTime, toleranceBefore: .zero, toleranceAfter: .zero) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index c633ccb801..479b04ed72 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -72,7 +72,6 @@ public final class MediaEditorVideoAVAssetWriter: MediaEditorVideoExportWriter { var videoSettings = configuration.videoSettings if var compressionSettings = videoSettings[AVVideoCompressionPropertiesKey] as? [String: Any] { compressionSettings[AVVideoExpectedSourceFrameRateKey] = sourceFrameRate -// compressionSettings[AVVideoMaxKeyFrameIntervalKey] = sourceFrameRate videoSettings[AVVideoCompressionPropertiesKey] = compressionSettings } @@ -221,12 +220,19 @@ public final class MediaEditorVideoExport { } var audioTimeRange: CMTimeRange? { - let offset = self.values.audioTrackOffset ?? 0.0 - if let range = self.values.audioTrackTrimRange { - return CMTimeRange( - start: CMTime(seconds: offset + range.lowerBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), - end: CMTime(seconds: offset + range.upperBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - ) + if let audioTrack = self.values.audioTrack { + let offset = self.values.audioTrackOffset ?? 0.0 + if let range = self.values.audioTrackTrimRange { + return CMTimeRange( + start: CMTime(seconds: offset + range.lowerBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), + end: CMTime(seconds: offset + range.upperBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + ) + } else { + return CMTimeRange( + start: CMTime(seconds: offset, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), + end: CMTime(seconds: offset + min(15.0, audioTrack.duration), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + ) + } } else { return nil } @@ -289,7 +295,7 @@ public final class MediaEditorVideoExport { private var additionalReader: AVAssetReader? private var videoOutput: AVAssetReaderOutput? - private var audioOutput: AVAssetReaderAudioMixOutput? + private var audioOutput: AVAssetReaderOutput? private var textureRotation: TextureRotation = .rotate0Degrees private var additionalVideoOutput: AVAssetReaderOutput? @@ -399,9 +405,7 @@ public final class MediaEditorVideoExport { print("error") return } - - - + let timeRange: CMTimeRange = CMTimeRangeMake(start: .zero, duration: duration) try? videoTrack.insertTimeRange(timeRange, of: videoAssetTrack, at: .zero) if let audioAssetTrack = asset.tracks(withMediaType: .audio).first, let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid), !self.configuration.values.videoIsMuted { @@ -542,12 +546,63 @@ public final class MediaEditorVideoExport { self.setupComposer() + var inputAudioMix: AVMutableAudioMix? + self.writer = MediaEditorVideoAVAssetWriter() guard let writer = self.writer else { return } writer.setup(configuration: self.configuration, outputPath: self.outputPath) writer.setupVideoInput(configuration: self.configuration, sourceFrameRate: 30.0) + + if let audioData = self.configuration.values.audioTrack { + let mixComposition = AVMutableComposition() + let audioPath = fullDraftPath(peerId: self.configuration.values.peerId, path: audioData.path) + let audioAsset = AVURLAsset(url: URL(fileURLWithPath: audioPath)) + + if let musicAssetTrack = audioAsset.tracks(withMediaType: .audio).first, + let musicTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) { + do { + let reader = try AVAssetReader(asset: mixComposition) + + var musicRange = CMTimeRange(start: .zero, duration: CMTime(seconds: min(15.0, audioData.duration), preferredTimescale: CMTimeScale(NSEC_PER_SEC))) + if let audioTrackRange = self.configuration.audioTimeRange { + musicRange = audioTrackRange + } + try? musicTrack.insertTimeRange(musicRange, of: musicAssetTrack, at: .zero) + + if let volume = self.configuration.values.audioTrackVolume, volume < 1.0 { + let audioMix = AVMutableAudioMix() + var audioMixParam: [AVMutableAudioMixInputParameters] = [] + let param: AVMutableAudioMixInputParameters = AVMutableAudioMixInputParameters(track: musicTrack) + param.trackID = musicTrack.trackID + param.setVolume(Float(volume), at: CMTime.zero) + audioMixParam.append(param) + audioMix.inputParameters = audioMixParam + inputAudioMix = audioMix + } + + let audioTracks = mixComposition.tracks(withMediaType: .audio) + let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil) + audioOutput.audioMix = inputAudioMix + audioOutput.alwaysCopiesSampleData = false + if reader.canAdd(audioOutput) { + reader.add(audioOutput) + + self.reader = reader + self.audioOutput = audioOutput + + writer.setupAudioInput(configuration: self.configuration) + } else { + self.internalStatus = .finished + self.statusValue = .failed(.addAudioOutput) + } + } catch { + self.internalStatus = .finished + self.statusValue = .failed(.addAudioOutput) + } + } + } } private func finish() { @@ -818,20 +873,53 @@ public final class MediaEditorVideoExport { return } + if let _ = self.audioOutput, let reader = self.reader { + guard reader.startReading() else { + self.statusValue = .failed(.reading(nil)) + return + } + } + self.internalStatus = .exporting writer.startSession(atSourceTime: .zero) - self.imageArguments = (5.0, Double(self.configuration.frameRate), CMTime(value: 0, timescale: Int32(self.configuration.frameRate))) + var duration: Double = 5.0 + if let audioDuration = self.configuration.audioTimeRange?.duration.seconds { + duration = audioDuration + } + self.imageArguments = (duration, Double(self.configuration.frameRate), CMTime(value: 0, timescale: Int32(self.configuration.frameRate))) + + var videoCompleted = false + var audioCompleted = false var exportForVideoOutput: MediaEditorVideoExport? = self writer.requestVideoDataWhenReady(on: self.queue.queue) { guard let export = exportForVideoOutput else { return } if !export.encodeImageVideo() { + videoCompleted = true exportForVideoOutput = nil - export.finish() + if audioCompleted { + export.finish() + } } } + + if let _ = self.audioOutput { + var exportForAudioOutput: MediaEditorVideoExport? = self + writer.requestAudioDataWhenReady(on: self.queue.queue) { + guard let export = exportForAudioOutput else { return } + if !export.encodeAudio() { + audioCompleted = true + exportForAudioOutput = nil + if videoCompleted { + export.finish() + } + } + } + } else { + audioCompleted = true + } } private func startVideoExport() { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 0c0c2aadec..3bd5173492 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1198,6 +1198,16 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) ) + if self.inputPanelExternalState.isEditing { + if let controller = self.environment?.controller() as? MediaEditorScreen { + if controller.node.entitiesView.hasSelection { + Queue.mainQueue().justDispatch { + controller.node.entitiesView.selectEntity(nil) + } + } + } + } + if self.inputPanelExternalState.isEditing { if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) @@ -1362,7 +1372,11 @@ final class MediaEditorScreenComponent: Component { self.addSubview(scrubberView) } } - bottomControlsTransition.setFrame(view: scrubberView, frame: scrubberFrame) + if animateIn { + scrubberView.frame = scrubberFrame + } else { + bottomControlsTransition.setFrame(view: scrubberView, frame: scrubberFrame) + } if !self.animatingButtons && !(isAudioOnly && animateIn) { transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption ? 0.0 : 1.0) } else if animateIn { @@ -3076,16 +3090,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } func presentAudioPicker() { + var isSettingTrack = false self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3"], forceDarkTheme: true, dismissed: { [weak self] in if let self { Queue.mainQueue().after(0.1) { - self.mediaEditor?.play() + if !isSettingTrack { + self.mediaEditor?.play() + } } } }, completion: { [weak self] urls in guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else { return } + isSettingTrack = true try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) @@ -3161,7 +3179,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate Queue.mainQueue().async { mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration)) if mediaEditor.sourceIsVideo { - if let videoDuration = mediaEditor.duration { + if let videoDuration = mediaEditor.originalDuration { mediaEditor.setAudioTrackTrimRange(0 ..< min(videoDuration, audioDuration), apply: true) } } else { @@ -3173,6 +3191,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if isScopedResource { url.stopAccessingSecurityScopedResource() } + + mediaEditor.play() } }) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index 0d325f851f..94e3e7e2c1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -249,10 +249,12 @@ final class VideoScrubberComponent: Component { self.transparentFramesContainer.alpha = 0.5 self.transparentFramesContainer.clipsToBounds = true self.transparentFramesContainer.layer.cornerRadius = 9.0 + self.transparentFramesContainer.isUserInteractionEnabled = false self.opaqueFramesContainer.clipsToBounds = true self.opaqueFramesContainer.layer.cornerRadius = 9.0 - + self.opaqueFramesContainer.isUserInteractionEnabled = false + self.addSubview(self.audioClippingView) self.audioClippingView.addSubview(self.audioScrollView) self.audioScrollView.addSubview(self.audioContainerView) @@ -383,13 +385,19 @@ final class VideoScrubberComponent: Component { guard let component = self.component else { return } + + var trimDuration = component.duration + if component.audioOnly, let audioData = component.audioData { + trimDuration = min(30.0, audioData.duration) + } + let location = gestureRecognizer.location(in: self) let start = handleWidth let end = self.frame.width - handleWidth let length = end - start let fraction = (location.x - start) / length - let position = max(component.startPosition, min(component.endPosition, component.duration * fraction)) + let position = max(component.startPosition, min(component.endPosition, trimDuration * fraction)) let transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: @@ -409,8 +417,16 @@ final class VideoScrubberComponent: Component { let cursorPositionFraction = duration > 0.0 ? position / duration : 0.0 let cursorPosition = floorToScreenPixels(handleWidth - 1.0 + (size.width - handleWidth * 2.0 + 2.0) * cursorPositionFraction) var cursorFrame = CGRect(origin: CGPoint(x: cursorPosition - handleWidth / 2.0, y: -5.0 - UIScreenPixel), size: CGSize(width: handleWidth, height: height)) - cursorFrame.origin.x = max(self.ghostTrimView.leftHandleView.frame.maxX - cursorPadding, cursorFrame.origin.x) - cursorFrame.origin.x = min(self.ghostTrimView.rightHandleView.frame.minX - handleWidth + cursorPadding, cursorFrame.origin.x) + + var leftEdge = self.ghostTrimView.leftHandleView.frame.maxX + var rightEdge = self.ghostTrimView.rightHandleView.frame.minX + if let component = self.component, component.audioOnly { + leftEdge = self.trimView.leftHandleView.frame.maxX + rightEdge = self.trimView.rightHandleView.frame.minX + } + + cursorFrame.origin.x = max(leftEdge - cursorPadding, cursorFrame.origin.x) + cursorFrame.origin.x = min(rightEdge - handleWidth + cursorPadding, cursorFrame.origin.x) return cursorFrame } @@ -420,6 +436,11 @@ final class VideoScrubberComponent: Component { } let timestamp = CACurrentMediaTime() + var trimDuration = component.duration + if component.audioOnly, let audioData = component.audioData { + trimDuration = min(30.0, audioData.duration) + } + let updatedPosition: Double if let (start, from, to, _) = self.positionAnimation { let duration = to - from @@ -433,7 +454,7 @@ final class VideoScrubberComponent: Component { updatedPosition = max(component.startPosition, min(component.endPosition, component.position + advance)) } let cursorHeight: CGFloat = component.audioData != nil ? 80.0 : 50.0 - self.cursorView.frame = cursorFrame(size: scrubberSize, height: cursorHeight, position: updatedPosition, duration: component.duration) + self.cursorView.frame = cursorFrame(size: scrubberSize, height: cursorHeight, position: updatedPosition, duration: trimDuration) } func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -504,7 +525,7 @@ final class VideoScrubberComponent: Component { } audioTransition.setAlpha(view: self.audioClippingView, alpha: audioAlpha) - var audioClipOrigin: CGFloat = 0.0 + var audioClipOrigin: CGFloat = -9.0 var audioClipWidth = availableSize.width + 18.0 var deselectedAudioClipWidth: CGFloat = 0.0 @@ -735,7 +756,7 @@ final class VideoScrubberComponent: Component { // if self.cursorView.alpha.isZero { // cursorPosition = component.startPosition // } - videoTransition.setFrame(view: self.cursorView, frame: cursorFrame(size: scrubberSize, height: cursorHeight, position: cursorPosition, duration: component.duration)) + videoTransition.setFrame(view: self.cursorView, frame: cursorFrame(size: scrubberSize, height: cursorHeight, position: cursorPosition, duration: trimDuration)) } else { if let (_, _, end, ended) = self.positionAnimation { if ended, component.position >= component.startPosition && component.position < end - 1.0 { @@ -788,6 +809,11 @@ final class VideoScrubberComponent: Component { let hitTestSlop = UIEdgeInsets(top: -8.0, left: -9.0, bottom: -8.0, right: -9.0) return self.bounds.inset(by: hitTestSlop).contains(point) } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 526c7f66a4..a4ed47280f 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -374,6 +374,7 @@ public final class MessageInputPanelComponent: Component { private let counter = ComponentView() private var disabledPlaceholder: ComponentView? + private var textClippingView = UIView() private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() @@ -440,12 +441,15 @@ public final class MessageInputPanelComponent: Component { self.gradientView = UIImageView() self.bottomGradientView = UIView() + self.textClippingView.clipsToBounds = true + super.init(frame: frame) self.addSubview(self.bottomGradientView) self.addSubview(self.gradientView) self.fieldBackgroundView.addSubview(self.vibrancyEffectView) self.addSubview(self.fieldBackgroundView) + self.addSubview(self.textClippingView) self.viewForOverlayContent = ViewForOverlayContent( ignoreHit: { [weak self] view, point in @@ -715,7 +719,7 @@ public final class MessageInputPanelComponent: Component { return value } }, - resetScrollOnFocusChange: component.style == .media, + isOneLineWhenUnfocused: component.style == .media, formatMenuAvailability: component.isFormattingLocked ? .locked : .available, lockedFormatAction: { component.presentTextFormattingTooltip?() @@ -799,6 +803,12 @@ public final class MessageInputPanelComponent: Component { transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame) self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition) + var textClippingFrame = fieldBackgroundFrame + if component.style == .media, !isEditing { + textClippingFrame.size.height -= 10.0 + } + transition.setFrame(view: self.textClippingView, frame: textClippingFrame) + let gradientFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX - fieldFrame.minX, y: -topGradientHeight), size: CGSize(width: availableSize.width - (fieldBackgroundFrame.minX - fieldFrame.minX), height: topGradientHeight + fieldBackgroundFrame.maxY + insets.bottom)) transition.setFrame(view: self.gradientView, frame: gradientFrame) transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset))) @@ -918,7 +928,7 @@ public final class MessageInputPanelComponent: Component { if let textFieldView = self.textField.view as? TextFieldComponent.View { if textFieldView.superview == nil { - self.addSubview(textFieldView) + self.textClippingView.addSubview(textFieldView) if let viewForOverlayContent = self.viewForOverlayContent { self.addSubview(viewForOverlayContent) @@ -932,7 +942,7 @@ public final class MessageInputPanelComponent: Component { } } } - let textFieldFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize) + let textFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textFieldSize) transition.setFrame(view: textFieldView, frame: textFieldFrame) transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift index 76feb978c6..8b13789179 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -1,886 +1 @@ -import Foundation -import UIKit -import SwiftSignalKit -import Display -import AnimationCache -import Accelerate -import simd -private func alignUp(size: Int, align: Int) -> Int { - precondition(((align - 1) & align) == 0, "Align must be a power of two") - - let alignmentMask = align - 1 - return (size + alignmentMask) & ~alignmentMask -} - -private extension Float { - func remap(fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float) -> Float { - guard (fromHigh - fromLow) != 0.0 else { - return 0.0 - } - return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) - } -} - -private func makePipelineState(device: MTLDevice, library: MTLLibrary, vertexProgram: String, fragmentProgram: String) -> MTLRenderPipelineState? { - guard let loadedVertexProgram = library.makeFunction(name: vertexProgram) else { - return nil - } - guard let loadedFragmentProgram = library.makeFunction(name: fragmentProgram) else { - return nil - } - - let pipelineStateDescriptor = MTLRenderPipelineDescriptor() - pipelineStateDescriptor.vertexFunction = loadedVertexProgram - pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram - pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm - guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) else { - return nil - } - - return pipelineState -} - -@available(iOS 13.0, *) -public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { - private final class LoadFrameTask { - let task: () -> () -> Void - - init(task: @escaping () -> () -> Void) { - self.task = task - } - } - - private final class TargetReference { - let id: Int64 - weak var value: MultiAnimationRenderTarget? - - init(_ value: MultiAnimationRenderTarget) { - self.value = value - self.id = value.id - } - } - - private final class TextureStoragePool { - struct Parameters { - let width: Int - let height: Int - let format: TextureStorage.Content.Format - } - - let parameters: Parameters - private var items: [TextureStorage.Content] = [] - private var cleanupTimer: Foundation.Timer? - private var lastTakeTimestamp: Double = 0.0 - - init(width: Int, height: Int, format: TextureStorage.Content.Format) { - self.parameters = Parameters(width: width, height: height, format: format) - - let cleanupTimer = Foundation.Timer(timeInterval: 2.0, repeats: true, block: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.collect() - }) - self.cleanupTimer = cleanupTimer - RunLoop.main.add(cleanupTimer, forMode: .common) - } - - deinit { - self.cleanupTimer?.invalidate() - } - - private func collect() { - let timestamp = CFAbsoluteTimeGetCurrent() - if timestamp - self.lastTakeTimestamp < 1.0 { - return - } - if self.items.count > 32 { - autoreleasepool { - var remainingItems: [Unmanaged] = [] - while self.items.count > 32 { - let item = self.items.removeLast() - remainingItems.append(Unmanaged.passRetained(item)) - } - DispatchQueue.global().async { - autoreleasepool { - for item in remainingItems { - item.release() - } - } - } - } - } - } - - func recycle(content: TextureStorage.Content) { - self.items.append(content) - } - - func take() -> TextureStorage? { - if self.items.isEmpty { - self.lastTakeTimestamp = CFAbsoluteTimeGetCurrent() - return nil - } - return TextureStorage(pool: self, content: self.items.removeLast()) - } - - static func takeNew(device: MTLDevice, parameters: Parameters, pool: TextureStoragePool) -> TextureStorage? { - guard let content = TextureStorage.Content(device: device, width: parameters.width, height: parameters.height, format: parameters.format) else { - return nil - } - return TextureStorage(pool: pool, content: content) - } - } - - private final class TextureStorage { - final class Content { - enum Format { - case bgra - case r - } - - let buffer: MTLBuffer? - - let width: Int - let height: Int - let bytesPerRow: Int - let texture: MTLTexture - - static func rowAlignment(device: MTLDevice, format: Format) -> Int { - let pixelFormat: MTLPixelFormat - switch format { - case .bgra: - pixelFormat = .bgra8Unorm - case .r: - pixelFormat = .r8Unorm - } - return device.minimumLinearTextureAlignment(for: pixelFormat) - } - - init?(device: MTLDevice, width: Int, height: Int, format: Format) { - let bytesPerPixel: Int - let pixelFormat: MTLPixelFormat - switch format { - case .bgra: - bytesPerPixel = 4 - pixelFormat = .bgra8Unorm - case .r: - bytesPerPixel = 1 - pixelFormat = .r8Unorm - } - let pixelRowAlignment = Content.rowAlignment(device: device, format: format) - let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment) - - self.width = width - self.height = height - self.bytesPerRow = bytesPerRow - - #if targetEnvironment(simulator) - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.pixelFormat = pixelFormat - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.usage = [.shaderRead] - textureDescriptor.storageMode = .shared - - guard let texture = device.makeTexture(descriptor: textureDescriptor) else { - return nil - } - self.buffer = nil - #else - guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else { - return nil - } - self.buffer = buffer - - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.pixelFormat = pixelFormat - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.usage = [.shaderRead] - textureDescriptor.storageMode = buffer.storageMode - - guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else { - return nil - } - #endif - - self.texture = texture - } - - func replace(rgbaData: Data, width: Int, height: Int, bytesPerRow: Int) { - if width != self.width || height != self.height { - assert(false, "Image size does not match") - return - } - let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1)) - - if let buffer = self.buffer, self.bytesPerRow == bytesPerRow { - assert(bytesPerRow * height <= rgbaData.count) - - rgbaData.withUnsafeBytes { bytes in - let _ = memcpy(buffer.contents(), bytes.baseAddress!, bytesPerRow * height) - } - } else { - rgbaData.withUnsafeBytes { bytes in - self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes.baseAddress!, bytesPerRow: bytesPerRow) - } - } - } - } - - private weak var pool: TextureStoragePool? - let content: Content - private var isInvalidated: Bool = false - - init(pool: TextureStoragePool, content: Content) { - self.pool = pool - self.content = content - } - - deinit { - if !self.isInvalidated { - self.pool?.recycle(content: self.content) - } - } - } - - private final class Frame { - let duration: Double - let textureY: TextureStorage - let textureU: TextureStorage - let textureV: TextureStorage - let textureA: TextureStorage - - var remainingDuration: Double - - init?(device: MTLDevice, textureY: TextureStorage, textureU: TextureStorage, textureV: TextureStorage, textureA: TextureStorage, data: AnimationCacheItemFrame, duration: Double) { - self.duration = duration - self.remainingDuration = duration - - self.textureY = textureY - self.textureU = textureU - self.textureV = textureV - self.textureA = textureA - - switch data.format { - case .rgba: - return nil - case let .yuva(y, u, v, a): - self.textureY.content.replace(rgbaData: y.data, width: y.width, height: y.height, bytesPerRow: y.bytesPerRow) - self.textureU.content.replace(rgbaData: u.data, width: u.width, height: u.height, bytesPerRow: u.bytesPerRow) - self.textureV.content.replace(rgbaData: v.data, width: v.width, height: v.height, bytesPerRow: v.bytesPerRow) - self.textureA.content.replace(rgbaData: a.data, width: a.width, height: a.height, bytesPerRow: a.bytesPerRow) - } - } - } - - private final class ItemContext { - static let queue = Queue(name: "MultiAnimationMetalRendererImpl", qos: .default) - - private let cache: AnimationCache - private let stateUpdated: () -> Void - - private var disposable: Disposable? - private var item: AnimationCacheItem? - - private(set) var currentFrame: Frame? - private var isLoadingFrame: Bool = false - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - var targets: [TargetReference] = [] - var slotIndex: Int - private let preferredRowAlignment: Int - - init(slotIndex: Int, preferredRowAlignment: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { - self.slotIndex = slotIndex - self.preferredRowAlignment = preferredRowAlignment - self.cache = cache - self.stateUpdated = stateUpdated - - self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - strongSelf.item = result.item - strongSelf.updateIsPlaying() - - if result.item == nil { - for target in strongSelf.targets { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: true) - } - } - } - } - }) - } - - deinit { - self.disposable?.dispose() - } - - func updateIsPlaying() { - var isPlaying = true - if self.item == nil { - isPlaying = false - } - - var shouldBeAnimating = false - for target in self.targets { - if let target = target.value { - if target.shouldBeAnimating { - shouldBeAnimating = true - break - } - } - } - if !shouldBeAnimating { - isPlaying = false - } - - self.isPlaying = isPlaying - } - - func animationTick(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? { - return self.update(device: device, texturePoolFullPlane: texturePoolFullPlane, texturePoolHalfPlane: texturePoolHalfPlane, advanceTimestamp: advanceTimestamp) - } - - private func update(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? { - guard let item = self.item else { - return nil - } - - if let currentFrame = self.currentFrame, !self.isLoadingFrame { - currentFrame.remainingDuration -= advanceTimestamp - } - - var frameAdvance: AnimationCacheItem.Advance? - if !self.isLoadingFrame { - if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { - let divisionFactor = advanceTimestamp / currentFrame.remainingDuration - let wholeFactor = round(divisionFactor) - if abs(wholeFactor - divisionFactor) < 0.005 { - currentFrame.remainingDuration = 0.0 - frameAdvance = .frames(Int(wholeFactor)) - } else { - currentFrame.remainingDuration -= advanceTimestamp - if currentFrame.remainingDuration <= 0.0 { - frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) - } - } - } else if self.currentFrame == nil { - frameAdvance = .frames(1) - } - } - - if let frameAdvance = frameAdvance, !self.isLoadingFrame { - self.isLoadingFrame = true - - let fullParameters = texturePoolFullPlane.parameters - let halfParameters = texturePoolHalfPlane.parameters - - let readyTextureY = texturePoolFullPlane.take() - let readyTextureU = texturePoolHalfPlane.take() - let readyTextureV = texturePoolHalfPlane.take() - let readyTextureA = texturePoolFullPlane.take() - let preferredRowAlignment = self.preferredRowAlignment - - return LoadFrameTask(task: { [weak self] in - let frame = item.advance(advance: frameAdvance, requestedFormat: .yuva(rowAlignment: preferredRowAlignment))?.frame - - let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) - let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) - let textureV = readyTextureV ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) - let textureA = readyTextureA ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) - - var currentFrame: Frame? - if let frame = frame, let textureY = textureY, let textureU = textureU, let textureV = textureV, let textureA = textureA { - currentFrame = Frame(device: device, textureY: textureY, textureU: textureU, textureV: textureV, textureA: textureA, data: frame, duration: frame.duration) - } - - return { - guard let strongSelf = self else { - return - } - - strongSelf.isLoadingFrame = false - - if let currentFrame = currentFrame { - strongSelf.currentFrame = currentFrame - } - } - }) - } - - return nil - } - } - - private final class SurfaceLayer: CAMetalLayer { - private let cellSize: CGSize - private let stateUpdated: () -> Void - - private let metalDevice: MTLDevice - private let commandQueue: MTLCommandQueue - private let renderPipelineState: MTLRenderPipelineState - - private let texturePoolFullPlane: TextureStoragePool - private let texturePoolHalfPlane: TextureStoragePool - - private let preferredRowAlignment: Int - - private let slotCount: Int - private let slotsX: Int - private let slotsY: Int - private var itemContexts: [String: ItemContext] = [:] - private var slotToItemId: [String?] - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - public init(cellSize: CGSize, stateUpdated: @escaping () -> Void) { - self.cellSize = cellSize - self.stateUpdated = stateUpdated - - let resolutionX = max(1, (1024 / Int(cellSize.width))) * Int(cellSize.width) - let resolutionY = max(1, (1024 / Int(cellSize.height))) * Int(cellSize.height) - self.slotsX = resolutionX / Int(cellSize.width) - self.slotsY = resolutionY / Int(cellSize.height) - let drawableSize = CGSize(width: cellSize.width * CGFloat(self.slotsX), height: cellSize.height * CGFloat(self.slotsY)) - - self.slotCount = (Int(drawableSize.width) / Int(cellSize.width)) * (Int(drawableSize.height) / Int(cellSize.height)) - self.slotToItemId = (0 ..< self.slotCount).map { _ in nil } - - self.metalDevice = MTLCreateSystemDefaultDevice()! - self.commandQueue = self.metalDevice.makeCommandQueue()! - - let mainBundle = Bundle(for: MultiAnimationMetalRendererImpl.self) - - guard let path = mainBundle.path(forResource: "MultiAnimationRendererBundle", ofType: "bundle") else { - preconditionFailure() - } - guard let bundle = Bundle(path: path) else { - preconditionFailure() - } - guard let defaultLibrary = try? self.metalDevice.makeDefaultLibrary(bundle: bundle) else { - preconditionFailure() - } - - self.renderPipelineState = makePipelineState(device: self.metalDevice, library: defaultLibrary, vertexProgram: "multiAnimationVertex", fragmentProgram: "multiAnimationFragment")! - - self.texturePoolFullPlane = TextureStoragePool(width: Int(self.cellSize.width), height: Int(self.cellSize.height), format: .r) - self.texturePoolHalfPlane = TextureStoragePool(width: Int(self.cellSize.width) / 2, height: Int(self.cellSize.height) / 2, format: .r) - - self.preferredRowAlignment = TextureStorage.Content.rowAlignment(device: self.metalDevice, format: .r) - - super.init() - - self.device = self.metalDevice - self.maximumDrawableCount = 2 - //self.metalLayer.presentsWithTransaction = true - self.contentsScale = 1.0 - - self.drawableSize = drawableSize - - self.pixelFormat = .bgra8Unorm - self.framebufferOnly = true - self.allowsNextDrawableTimeout = true - self.isOpaque = false - } - - override public init(layer: Any) { - preconditionFailure() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func action(forKey event: String) -> CAAction? { - return nullAction - } - - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable? { - if size != self.cellSize { - return nil - } - - let targetId = target.id - - if self.itemContexts[itemId] == nil { - for i in 0 ..< self.slotCount { - if self.slotToItemId[i] == nil { - self.slotToItemId[i] = itemId - self.itemContexts[itemId] = ItemContext(slotIndex: i, preferredRowAlignment: self.preferredRowAlignment, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - break - } - } - } - - if let itemContext = self.itemContexts[itemId] { - itemContext.targets.append(TargetReference(target)) - - let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let currentItemContext = strongSelf.itemContexts[itemId], currentItemContext === itemContext else { - return - } - strongSelf.removeTargetFromItemContext(itemId: itemId, itemContext: currentItemContext, targetId: targetId) - } - } - - let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in - guard let itemContext = itemContext else { - return - } - itemContext.updateIsPlaying() - } - - target.contents = self.contents - - let slotX = itemContext.slotIndex % self.slotsX - let slotY = itemContext.slotIndex / self.slotsX - let totalX = CGFloat(self.slotsX) * self.cellSize.width - let totalY = CGFloat(self.slotsY) * self.cellSize.height - let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY)) - target.contentsRect = contentsRect - - self.isPlaying = true - - return ActionDisposable { [weak self, weak target, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let currentItemContext = strongSelf.itemContexts[itemId], currentItemContext === itemContext else { - return - } - - if let target = target { - target.deinitCallbacks.remove(deinitIndex) - target.updateStateCallbacks.remove(updateStateIndex) - } - - strongSelf.removeTargetFromItemContext(itemId: itemId, itemContext: currentItemContext, targetId: targetId) - } - } - } else { - return nil - } - } - - private func removeTargetFromItemContext(itemId: String, itemContext: ItemContext, targetId: Int64) { - if let index = itemContext.targets.firstIndex(where: { $0.id == targetId }) { - itemContext.targets.remove(at: index) - - if itemContext.targets.isEmpty { - self.slotToItemId[itemContext.slotIndex] = nil - self.itemContexts.removeValue(forKey: itemId) - - if self.itemContexts.isEmpty { - self.isPlaying = false - } - } - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> [LoadFrameTask] { - var tasks: [LoadFrameTask] = [] - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - if let task = itemContext.animationTick(device: self.metalDevice, texturePoolFullPlane: self.texturePoolFullPlane, texturePoolHalfPlane: self.texturePoolHalfPlane, advanceTimestamp: advanceTimestamp) { - tasks.append(task) - } - } - } - - return tasks - } - - func redraw() { - guard let drawable = self.nextDrawable() else { - return - } - - let commandQueue = self.commandQueue - let renderPipelineState = self.renderPipelineState - let cellSize = self.cellSize - - guard let commandBuffer = commandQueue.makeCommandBuffer() else { - return - } - - /*let drawTime = CACurrentMediaTime() - timestamp - if drawTime > 9.0 / 1000.0 { - print("get time \(drawTime * 1000.0)") - }*/ - - let renderPassDescriptor = MTLRenderPassDescriptor() - renderPassDescriptor.colorAttachments[0].texture = drawable.texture - renderPassDescriptor.colorAttachments[0].loadAction = .clear - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0 - ) - - guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { - return - } - - var usedTextures: [Unmanaged] = [] - - var vertices: [Float] = [ - -1.0, -1.0, 0.0, 0.0, - 1.0, -1.0, 1.0, 0.0, - -1.0, 1.0, 0.0, 1.0, - 1.0, 1.0, 1.0, 1.0 - ] - - renderEncoder.setRenderPipelineState(renderPipelineState) - - var resolution = simd_uint2(UInt32(drawable.texture.width), UInt32(drawable.texture.height)) - renderEncoder.setVertexBytes(&resolution, length: MemoryLayout.size * 2, index: 1) - - var slotSize = simd_uint2(UInt32(cellSize.width), UInt32(cellSize.height)) - renderEncoder.setVertexBytes(&slotSize, length: MemoryLayout.size * 2, index: 2) - - for (_, itemContext) in self.itemContexts { - guard let frame = itemContext.currentFrame else { - continue - } - - let slotX = itemContext.slotIndex % self.slotsX - let slotY = self.slotsY - 1 - itemContext.slotIndex / self.slotsY - let totalX = CGFloat(self.slotsX) * self.cellSize.width - let totalY = CGFloat(self.slotsY) * self.cellSize.height - - let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY)) - - vertices[4 * 2 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 2 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - vertices[4 * 3 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 3 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - vertices[4 * 0 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 0 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - vertices[4 * 1 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 1 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0) - - var slotPosition = simd_uint2(UInt32(itemContext.slotIndex % self.slotsX), UInt32(itemContext.slotIndex % self.slotsY)) - renderEncoder.setVertexBytes(&slotPosition, length: MemoryLayout.size * 2, index: 3) - - usedTextures.append(Unmanaged.passRetained(frame.textureY)) - usedTextures.append(Unmanaged.passRetained(frame.textureU)) - usedTextures.append(Unmanaged.passRetained(frame.textureV)) - usedTextures.append(Unmanaged.passRetained(frame.textureA)) - renderEncoder.setFragmentTexture(frame.textureY.content.texture, index: 0) - renderEncoder.setFragmentTexture(frame.textureU.content.texture, index: 1) - renderEncoder.setFragmentTexture(frame.textureV.content.texture, index: 2) - renderEncoder.setFragmentTexture(frame.textureA.content.texture, index: 3) - - renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1) - } - - renderEncoder.endEncoding() - - if self.presentsWithTransaction { - if Thread.isMainThread { - commandBuffer.commit() - commandBuffer.waitUntilScheduled() - drawable.present() - } else { - CATransaction.begin() - commandBuffer.commit() - commandBuffer.waitUntilScheduled() - drawable.present() - CATransaction.commit() - } - } else { - commandBuffer.addScheduledHandler { _ in - drawable.present() - } - commandBuffer.addCompletedHandler { _ in - DispatchQueue.main.async { - for texture in usedTextures { - texture.release() - } - } - } - commandBuffer.commit() - } - } - } - - private var nextSurfaceLayerIndex: Int = 1 - private var surfaceLayers: [Int: SurfaceLayer] = [:] - - private var frameSkip: Int - private var displayLink: ConstantDisplayLinkAnimator? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - if self.isPlaying { - if self.displayLink == nil { - self.displayLink = ConstantDisplayLinkAnimator { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animationTick() - } - self.displayLink?.frameInterval = self.frameSkip - self.displayLink?.isPaused = false - } - } else { - if let displayLink = self.displayLink { - self.displayLink = nil - displayLink.invalidate() - } - } - } - } - } - - public init() { - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { - self.frameSkip = 1 - } else { - self.frameSkip = 2 - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, surfaceLayer) in self.surfaceLayers { - if surfaceLayer.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - assert(Thread.isMainThread) - - let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16))) - - for (_, surfaceLayer) in self.surfaceLayers { - if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, unique: unique, size: alignedSize, fetch: fetch) { - return disposable - } - } - - let index = self.nextSurfaceLayerIndex - self.nextSurfaceLayerIndex += 1 - let surfaceLayer = SurfaceLayer(cellSize: alignedSize, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.surfaceLayers[index] = surfaceLayer - if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, unique: unique, size: alignedSize, fetch: fetch) { - return disposable - } else { - return EmptyDisposable - } - } - - public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - return false - } - - public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - completion(false, true) - - return EmptyDisposable - } - - public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - } - - private func animationTick() { - let secondsPerFrame = Double(self.frameSkip) / 60.0 - - var tasks: [LoadFrameTask] = [] - var surfaceLayersWithTasks: [Int] = [] - for (index, surfaceLayer) in self.surfaceLayers { - var hasTasks = false - if surfaceLayer.isPlaying { - let surfaceLayerTasks = surfaceLayer.animationTick(advanceTimestamp: secondsPerFrame) - if !surfaceLayerTasks.isEmpty { - tasks.append(contentsOf: surfaceLayerTasks) - hasTasks = true - } - } - if hasTasks { - surfaceLayersWithTasks.append(index) - } - } - - if !tasks.isEmpty { - ItemContext.queue.async { [weak self] in - var completions: [() -> Void] = [] - for task in tasks { - let complete = task.task() - completions.append(complete) - } - - if !completions.isEmpty { - Queue.mainQueue().async { - for completion in completions { - completion() - } - - if let strongSelf = self { - for index in surfaceLayersWithTasks { - if let surfaceLayer = strongSelf.surfaceLayers[index] { - surfaceLayer.redraw() - } - } - } - } - } - } - } - } -} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 6c2b72a6ed..bde9a11206 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -9,6 +9,7 @@ public protocol MultiAnimationRenderer: AnyObject { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) } @@ -600,6 +601,34 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { }) } + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in + guard let item = item.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.image.cgImage { + completion(cgImage) + } + } else { + completion(nil) + } + } + }) + } + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) @@ -737,6 +766,23 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) } + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) + } + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { if let groupContext = self.groupContext { groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 38d97818b2..415c3dca0c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -92,6 +92,7 @@ swift_library( "//submodules/TelegramUI/Components/OptionButtonComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/AnimatedCountLabelNode", + "//submodules/StickerResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 0f0db76c1e..088eba59ad 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -18,36 +18,209 @@ import TextFormat import AnimatedCountLabelNode import LottieComponent import LottieComponentResourceContent +import StickerResources +import AnimationCache -public final class StaticStoryItemOverlaysView: UIImageView { - override public init(frame: CGRect) { - super.init(frame: frame) - } +private let shadowImage: UIImage = { + return UIImage(bundleImageName: "Stories/ReactionShadow")! +}() + +private let coverImage: UIImage = { + return UIImage(bundleImageName: "Stories/ReactionOutline")! +}() + +private let darkCoverImage: UIImage = { + return generateTintedImage(image: UIImage(bundleImageName: "Stories/ReactionOutline"), color: UIColor(rgb: 0x000000, alpha: 0.5))! +}() + +public func storyPreviewWithAddedReactions( + context: AccountContext, + storyItem: Stories.Item, + signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> +) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + var reactionData: [Signal<(MessageReaction.Reaction, CGImage?), NoError>] = [] - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - - } - - public func update( - context: AccountContext, - peer: EnginePeer, - story: EngineStoryItem, - availableReactions: StoryAvailableReactions?, - entityFiles: [MediaId: TelegramMediaFile] - ) { - - } - - override public func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { - return + let loadFile: (MessageReaction.Reaction, TelegramMediaFile) -> Signal<(MessageReaction.Reaction, CGImage?), NoError> = { reaction, file in + return Signal { subscriber in + let isTemplate = !"".isEmpty + return context.animationRenderer.loadFirstFrameAsImage(cache: context.animationCache, itemId: file.resource.id.stringRepresentation, size: CGSize(width: 128.0, height: 128.0), fetch: animationCacheFetchFile(postbox: context.account.postbox, userLocation: .other, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { result in + subscriber.putNext((reaction, result)) + if result != nil { + subscriber.putCompletion() + } + }) } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 !== rhs.1 { + return false + } + return true + }) + } + + var availableReactions: Promise? + var processedReactions: [MessageReaction.Reaction] = [] + var customFileIds: [Int64] = [] + for mediaArea in storyItem.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea { + if processedReactions.contains(reaction) { + continue + } + processedReactions.append(reaction) + + switch reaction { + case .builtin: + if availableReactions == nil { + availableReactions = Promise() + availableReactions?.set(context.engine.stickers.availableReactions()) + } + reactionData.append(availableReactions!.get() + |> take(1) + |> mapToSignal { availableReactions -> Signal<(MessageReaction.Reaction, CGImage?), NoError> in + guard let availableReactions else { + return .single((reaction, nil)) + } + for item in availableReactions.reactions { + if item.value == reaction { + guard let file = item.centerAnimation else { + break + } + return loadFile(reaction, file) + } + } + return .single((reaction, nil)) + }) + case let .custom(fileId): + if !customFileIds.contains(fileId) { + customFileIds.append(fileId) + } + } + } + } + + if !customFileIds.isEmpty { + let customFiles = Promise<[Int64: TelegramMediaFile]>() + customFiles.set(context.engine.stickers.resolveInlineStickers(fileIds: customFileIds)) - let _ = context + for id in customFileIds { + reactionData.append(customFiles.get() + |> take(1) + |> mapToSignal { customFiles -> Signal<(MessageReaction.Reaction, CGImage?), NoError> in + let reaction: MessageReaction.Reaction = .custom(id) + + guard let file = customFiles[id] else { + return .single((reaction, nil)) + } + + return loadFile(reaction, file) + }) + } + } + + return combineLatest( + signal, + combineLatest(reactionData) + ) + |> map { draw, reactionsData in + return { arguments in + guard let context = draw(arguments) else { + return nil + } + + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withContext { c in + c.concatenate(c.ctm.inverted()) + c.scaleBy(x: context.scale, y: context.scale) + } + + context.withFlippedContext { c in + c.setBlendMode(.normal) + + for mediaArea in storyItem.mediaAreas { + c.saveGState() + defer { + c.restoreGState() + } + + if case let .reaction(coordinates, reaction, flags) = mediaArea { + let _ = reaction + let _ = flags + + let referenceSize = fittedRect.size + var areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height) + areaSize.width *= 0.97 + areaSize.height *= 0.97 + let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height) + if targetFrame.width < 2.0 || targetFrame.height < 2.0 { + continue + } + + c.saveGState() + + c.translateBy(x: targetFrame.midX, y: targetFrame.midY) + c.scaleBy(x: flags.contains(.isFlipped) ? -1.0 : 1.0, y: -1.0) + c.rotate(by: -coordinates.rotation * (CGFloat.pi / 180.0)) + c.translateBy(x: -targetFrame.midX, y: -targetFrame.midY) + + let insets = UIEdgeInsets(top: -0.08, left: -0.05, bottom: -0.01, right: -0.02) + let coverFrame = CGRect(origin: CGPoint(x: targetFrame.width * insets.left, y: targetFrame.height * insets.top), size: CGSize(width: targetFrame.width - targetFrame.width * insets.left - targetFrame.width * insets.right, height: targetFrame.height - targetFrame.height * insets.top - targetFrame.height * insets.bottom)).offsetBy(dx: targetFrame.minX, dy: targetFrame.minY) + + c.draw(shadowImage.cgImage!, in: coverFrame) + + if flags.contains(.isDark) { + c.draw(darkCoverImage.cgImage!, in: coverFrame) + } else { + c.draw(coverImage.cgImage!, in: coverFrame) + } + + c.restoreGState() + + c.translateBy(x: targetFrame.midX, y: targetFrame.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.rotate(by: -coordinates.rotation * (CGFloat.pi / 180.0)) + c.translateBy(x: -targetFrame.midX, y: -targetFrame.midY) + + let minSide = floor(min(200.0, min(targetFrame.width, targetFrame.height)) * 0.5) + let itemSize = CGSize(width: minSide, height: minSide) + + if let (_, maybeImage) = reactionsData.first(where: { $0.0 == reaction }), let image = maybeImage { + var imageFrame = itemSize.centered(around: targetFrame.center.offsetBy(dx: 0.0, dy: -targetFrame.height * 0.05)) + if case .builtin = reaction { + imageFrame = imageFrame.insetBy(dx: -imageFrame.width * 0.5, dy: -imageFrame.height * 0.5) + } + + c.draw(image, in: imageFrame) + } + } + } + } + + context.withContext { c in + c.concatenate(c.ctm.inverted()) + c.scaleBy(x: context.scale, y: context.scale) + + c.scaleBy(x: context.size.width * 0.5, y: context.size.height * 0.5) + c.scaleBy(x: 1.0, y: -1.0) + c.scaleBy(x: -context.size.width * 0.5, y: -context.size.height * 0.5) + } + + addCorners(context, arguments: arguments) + + return context + } } } @@ -56,14 +229,6 @@ final class StoryItemOverlaysView: UIView { return Font.with(size: 17.0, design: .camera, weight: .semibold, traits: .monospacedNumbers) }() - private static let shadowImage: UIImage = { - return UIImage(bundleImageName: "Stories/ReactionShadow")! - }() - - private static let coverImage: UIImage = { - return UIImage(bundleImageName: "Stories/ReactionOutline")! - }() - private final class ItemView: HighlightTrackingButton { private let shadowView: UIImageView private let coverView: UIImageView @@ -83,8 +248,8 @@ final class StoryItemOverlaysView: UIView { private var customEmojiLoadDisposable: Disposable? override init(frame: CGRect) { - self.shadowView = UIImageView(image: StoryItemOverlaysView.shadowImage) - self.coverView = UIImageView(image: StoryItemOverlaysView.coverImage) + self.shadowView = UIImageView(image: shadowImage) + self.coverView = UIImageView(image: coverImage) super.init(frame: frame) diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 93284de3ad..4950e7c9fa 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -91,7 +91,7 @@ public final class TextFieldComponent: Component { public let insets: UIEdgeInsets public let hideKeyboard: Bool public let resetText: NSAttributedString? - public let resetScrollOnFocusChange: Bool + public let isOneLineWhenUnfocused: Bool public let formatMenuAvailability: FormatMenuAvailability public let lockedFormatAction: () -> Void public let present: (ViewController) -> Void @@ -106,7 +106,7 @@ public final class TextFieldComponent: Component { insets: UIEdgeInsets, hideKeyboard: Bool, resetText: NSAttributedString?, - resetScrollOnFocusChange: Bool, + isOneLineWhenUnfocused: Bool, formatMenuAvailability: FormatMenuAvailability, lockedFormatAction: @escaping () -> Void, present: @escaping (ViewController) -> Void, @@ -120,7 +120,7 @@ public final class TextFieldComponent: Component { self.insets = insets self.hideKeyboard = hideKeyboard self.resetText = resetText - self.resetScrollOnFocusChange = resetScrollOnFocusChange + self.isOneLineWhenUnfocused = isOneLineWhenUnfocused self.formatMenuAvailability = formatMenuAvailability self.lockedFormatAction = lockedFormatAction self.present = present @@ -149,7 +149,7 @@ public final class TextFieldComponent: Component { if lhs.resetText != rhs.resetText { return false } - if lhs.resetScrollOnFocusChange != rhs.resetScrollOnFocusChange { + if lhs.isOneLineWhenUnfocused != rhs.isOneLineWhenUnfocused { return false } if lhs.formatMenuAvailability != rhs.formatMenuAvailability { @@ -201,6 +201,8 @@ public final class TextFieldComponent: Component { private var customEmojiContainerView: CustomEmojiContainerView? private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? + private let ellipsisView = ComponentView() + private var inputState: InputState { let selectionRange: Range = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length) return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange) @@ -796,6 +798,30 @@ public final class TextFieldComponent: Component { component.externalState.hasTrackingView = hasTrackingView } + func rightmostPositionOfFirstLine() -> CGPoint? { + let glyphRange = self.layoutManager.glyphRange(for: self.textContainer) + + if glyphRange.length == 0 { return nil } + + var lineRect = CGRect.zero + var glyphIndexForStringStart = glyphRange.location + var lineRange: NSRange = NSRange() + + repeat { + lineRect = self.layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndexForStringStart, effectiveRange: &lineRange) + if NSMaxRange(lineRange) > glyphRange.length { + lineRange.length = glyphRange.length - lineRange.location + } + glyphIndexForStringStart = NSMaxRange(lineRange) + } while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange) + + let padding = self.textView.textContainerInset.left + let rightmostX = lineRect.maxX + padding + let rightmostY = lineRect.minY + self.textView.textContainerInset.top + + return CGPoint(x: rightmostX, y: rightmostY) + } + func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state @@ -834,7 +860,7 @@ public final class TextFieldComponent: Component { let isEditing = self.textView.isFirstResponder var refreshScrolling = self.textView.bounds.size != size - if component.resetScrollOnFocusChange && !isEditing && isEditing != wasEditing { + if component.isOneLineWhenUnfocused && !isEditing && isEditing != wasEditing { refreshScrolling = true } self.textView.frame = CGRect(origin: CGPoint(), size: size) @@ -844,7 +870,7 @@ public final class TextFieldComponent: Component { if refreshScrolling { if isEditing { - if wasEditing { + if wasEditing || component.isOneLineWhenUnfocused { self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false) } } else { @@ -872,6 +898,35 @@ public final class TextFieldComponent: Component { } } + if component.isOneLineWhenUnfocused, let position = self.rightmostPositionOfFirstLine() { + let ellipsisSize = self.ellipsisView.update( + transition: transition, + component: AnyComponent( + Text( + text: "\u{2026}", + font: Font.regular(component.fontSize), + color: component.textColor + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.ellipsisView.view { + if view.superview == nil { + self.textView.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: position.x - 11.0, y: position.y), size: ellipsisSize)) + + let ellipsisTransition: Transition + if isEditing { + ellipsisTransition = .easeInOut(duration: 0.2) + } else { + ellipsisTransition = .easeInOut(duration: 0.3) + } + ellipsisTransition.setAlpha(view: view, alpha: isEditing ? 0.0 : 1.0) + } + } + self.updateEntities() return size diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json deleted file mode 100644 index ef12df2de0..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "SecretMediaIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "SecretMediaIcon@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@2x.png deleted file mode 100644 index 9c98438f62..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@3x.png deleted file mode 100644 index cbf683dca1..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json new file mode 100644 index 0000000000..9ca42ca52a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "viewonce_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf new file mode 100644 index 0000000000..cc28878464 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf @@ -0,0 +1,212 @@ +%PDF-1.7 + +1 0 obj + << /ExtGState << /E4 << /ca 0.200000 >> + /E2 << /ca 0.600000 >> + /E3 << /ca 0.400000 >> + /E1 << /ca 0.800000 >> + >> >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 3.670105 cm +0.000000 0.000000 0.000000 scn +10.000000 0.664955 m +10.367270 0.664955 10.665000 0.962687 10.665000 1.329956 c +10.665000 1.697226 10.367270 1.994957 10.000000 1.994957 c +10.000000 0.664955 l +h +10.000000 20.664955 m +10.367270 20.664955 10.665000 20.962687 10.665000 21.329956 c +10.665000 21.697226 10.367270 21.994957 10.000000 21.994957 c +10.000000 20.664955 l +h +10.000000 1.994957 m +4.844422 1.994957 0.665000 6.174377 0.665000 11.329956 c +-0.665000 11.329956 l +-0.665000 5.439838 4.109883 0.664955 10.000000 0.664955 c +10.000000 1.994957 l +h +0.665000 11.329956 m +0.665000 16.485535 4.844422 20.664955 10.000000 20.664955 c +10.000000 21.994957 l +4.109883 21.994957 -0.665000 17.220074 -0.665000 11.329956 c +0.665000 11.329956 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 13.462646 4.335022 cm +0.000000 0.000000 0.000000 scn +0.607792 21.290033 m +0.914249 21.316515 1.224271 21.330017 1.537287 21.330017 c +1.850302 21.330017 2.160324 21.316515 2.466781 21.290033 c +2.832687 21.258417 3.103682 20.936161 3.072065 20.570255 c +3.040448 20.204350 2.718192 19.933353 2.352286 19.964972 c +2.083856 19.988165 1.812035 20.000017 1.537287 20.000017 c +1.262538 20.000017 0.990717 19.988165 0.722287 19.964972 c +0.356381 19.933353 0.034125 20.204350 0.002508 20.570255 c +-0.029109 20.936161 0.241886 21.258417 0.607792 21.290033 c +h +0.002508 0.759779 m +0.034125 1.125685 0.356381 1.396679 0.722287 1.365063 c +0.990717 1.341867 1.262538 1.330017 1.537287 1.330017 c +1.812035 1.330017 2.083856 1.341867 2.352286 1.365063 c +2.718192 1.396679 3.040448 1.125685 3.072065 0.759779 c +3.103682 0.393873 2.832687 0.071617 2.466781 0.039999 c +2.160324 0.013519 1.850302 0.000015 1.537287 0.000015 c +1.224271 0.000015 0.914249 0.013519 0.607792 0.039999 c +0.241886 0.071617 -0.029109 0.393873 0.002508 0.759779 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 18.562012 5.086609 cm +0.000000 0.000000 0.000000 scn +2.718522 2.103071 m +2.929436 1.802403 2.856679 1.387682 2.556010 1.176766 c +2.050149 0.821909 1.511930 0.509789 0.946597 0.245714 c +0.613840 0.090281 0.218083 0.234030 0.062649 0.566786 c +-0.092785 0.899544 0.050963 1.295300 0.383720 1.450733 c +0.878228 1.681723 1.349272 1.954861 1.792216 2.265581 c +2.092885 2.476498 2.507605 2.403738 2.718522 2.103071 c +h +f* +n +Q +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 22.527100 8.415771 cm +0.000000 0.000000 0.000000 scn +1.819443 2.959568 m +2.152200 2.804134 2.295949 2.408377 2.140515 2.075620 c +1.876443 1.510287 1.564322 0.972069 1.209464 0.466206 c +0.998547 0.165539 0.583827 0.092780 0.283160 0.303696 c +-0.017509 0.514613 -0.090267 0.929333 0.120648 1.230002 c +0.431370 1.672945 0.704507 2.143989 0.935496 2.638497 c +1.090930 2.971253 1.486687 3.115002 1.819443 2.959568 c +h +f* +n +Q +q +/E2 gs +1.000000 0.000000 -0.000000 1.000000 24.297363 13.404541 cm +0.000000 0.000000 0.000000 scn +0.607792 3.130305 m +0.973697 3.161922 1.295953 2.890927 1.327572 2.525021 c +1.354051 2.218563 1.367555 1.908542 1.367555 1.595526 c +1.367555 1.282510 1.354051 0.972489 1.327572 0.666031 c +1.295953 0.300125 0.973697 0.029130 0.607792 0.060747 c +0.241886 0.092365 -0.029108 0.414621 0.002508 0.780526 c +0.025703 1.048956 0.037553 1.320777 0.037553 1.595526 c +0.037553 1.870275 0.025703 2.142096 0.002508 2.410526 c +-0.029108 2.776431 0.241886 3.098687 0.607792 3.130305 c +h +f* +n +Q +q +/E3 gs +1.000000 0.000000 -0.000000 1.000000 22.527100 18.379028 cm +0.000000 0.000000 0.000000 scn +0.283159 2.901568 m +0.583828 3.112484 0.998548 3.039725 1.209465 2.739057 c +1.564321 2.233195 1.876442 1.694978 2.140516 1.129645 c +2.295950 0.796888 2.152200 0.401131 1.819444 0.245697 c +1.486686 0.090263 1.090931 0.234012 0.935497 0.566768 c +0.704508 1.061275 0.431369 1.532320 0.120649 1.975264 c +-0.090267 2.275931 -0.017508 2.690652 0.283159 2.901568 c +h +f* +n +Q +q +/E4 gs +1.000000 0.000000 -0.000000 1.000000 18.562012 22.344177 cm +0.000000 0.000000 0.000000 scn +0.062649 2.002510 m +0.218083 2.335267 0.613840 2.479015 0.946597 2.323581 c +1.511930 2.059509 2.050148 1.747388 2.556011 1.392531 c +2.856678 1.181615 2.929437 0.766894 2.718521 0.466226 c +2.507604 0.165558 2.092884 0.092800 1.792215 0.303715 c +1.349271 0.614436 0.878228 0.887573 0.383720 1.118563 c +0.050964 1.273997 -0.092785 1.669754 0.062649 2.002510 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 12.500000 9.737305 cm +0.000000 0.000000 0.000000 scn +2.718506 0.000000 m +2.324219 0.000000 2.075195 0.290527 2.075195 0.715942 c +2.075195 8.443481 l +1.099854 7.727539 l +0.913086 7.571899 0.809326 7.530396 0.601807 7.530396 c +0.269775 7.530396 0.000000 7.820923 0.000000 8.163330 c +0.000000 8.401978 0.114136 8.588745 0.352783 8.765137 c +1.774292 9.730103 l +2.147827 9.979126 2.355347 10.072510 2.645874 10.072510 c +3.112793 10.072510 3.372192 9.792358 3.372192 9.263184 c +3.372192 0.715942 l +3.372192 0.280151 3.123169 0.000000 2.718506 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 4922 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.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 +0000000216 00000 n +0000005194 00000 n +0000005217 00000 n +0000005390 00000 n +0000005464 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5523 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf new file mode 100644 index 0000000000..ea8526a585 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf @@ -0,0 +1,163 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R + /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] + /Domain [ 0.000000 1.000000 ] + /FunctionType 4 + >> +stream +{ 0.419608 exch 0.576471 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub 0.392998 mul 0.419608 add exch dup 0.000000 sub -0.321544 mul 0.576471 add exch dup 0.000000 sub 0.000000 mul 1.000000 add exch } if dup 0.439058 gt { exch pop exch pop exch pop dup 0.439058 sub 0.538311 mul 0.592157 add exch dup 0.439058 sub -0.034955 mul 0.435294 add exch dup 0.439058 sub -0.342561 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.894118 exch 0.415686 exch 0.807843 exch } if pop } +endstream +endobj + +2 0 obj + 533 +endobj + +3 0 obj + << /Pattern << /P1 << /Matrix [ 105.698799 22.310228 -22.310228 105.698799 -4.867018 -86.125580 ] + /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] + /ColorSpace /DeviceRGB + /Function 1 0 R + /Domain [ 0.000000 1.000000 ] + /ShadingType 2 + /Extend [ true true ] + >> + /PatternType 2 + /Type /Pattern + >> >> >> +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +q +1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm +0.949020 0.949020 0.968627 scn +22.340000 15.320000 m +22.340000 9.609375 17.710625 4.980000 12.000000 4.980000 c +12.000000 1.660000 l +19.544210 1.660000 25.660000 7.775789 25.660000 15.320000 c +22.340000 15.320000 l +h +12.000000 4.980000 m +6.289376 4.980000 1.660000 9.609375 1.660000 15.320000 c +-1.660000 15.320000 l +-1.660000 7.775789 4.455791 1.660000 12.000000 1.660000 c +12.000000 4.980000 l +h +1.660000 15.320000 m +1.660000 21.030624 6.289376 25.660000 12.000000 25.660000 c +12.000000 28.980000 l +4.455791 28.980000 -1.660000 22.864208 -1.660000 15.320000 c +1.660000 15.320000 l +h +12.000000 25.660000 m +17.710625 25.660000 22.340000 21.030624 22.340000 15.320000 c +25.660000 15.320000 l +25.660000 22.864208 19.544210 28.980000 12.000000 28.980000 c +12.000000 25.660000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm +0.850980 0.850980 0.850980 scn +24.000000 15.320000 m +24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c +5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c +0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c +18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm +/Pattern cs +/P1 scn +24.000000 15.320000 m +24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c +5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c +0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c +18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c +h +f +n +Q +Q +q +1.000000 0.000000 -0.000000 1.000000 8.793701 5.165123 cm +1.000000 1.000000 1.000000 scn +6.639333 9.995974 m +6.270643 9.995974 5.989172 10.325375 6.046674 10.689552 c +6.848394 15.767110 l +6.948168 16.399014 6.121798 16.727470 5.760551 16.199497 c +0.105843 7.934922 l +-0.166615 7.536714 0.118531 6.996113 0.601028 6.996113 c +3.087691 6.996113 l +3.456380 6.996113 3.737850 6.666713 3.680349 6.302535 c +2.878629 1.224977 l +2.778855 0.593074 3.605225 0.264616 3.966471 0.792590 c +9.621180 9.057164 l +9.893639 9.455373 9.608493 9.995974 9.125995 9.995974 c +6.639333 9.995974 l +h +f* +n +Q + +endstream +endobj + +5 0 obj + 2168 +endobj + +6 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 27.319946 27.320000 ] + /Resources 3 0 R + /Contents 4 0 R + /Parent 7 0 R + >> +endobj + +7 0 obj + << /Kids [ 6 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +8 0 obj + << /Pages 7 0 R + /Type /Catalog + >> +endobj + +xref +0 9 +0000000000 65535 f +0000000010 00000 n +0000000727 00000 n +0000000749 00000 n +0000001379 00000 n +0000003603 00000 n +0000003626 00000 n +0000003799 00000 n +0000003873 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 8 0 R + /Size 9 +>> +startxref +3932 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json new file mode 100644 index 0000000000..17f972537b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AvatarBoost.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf new file mode 100644 index 0000000000..a6b8db9df8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf @@ -0,0 +1,79 @@ +%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 1.889648 0.842155 cm +1.000000 1.000000 1.000000 scn +11.065588 16.659954 m +10.451106 16.659954 9.981988 17.208954 10.077824 17.815918 c +11.413990 26.278299 l +11.580280 27.331470 10.202996 27.878902 9.600920 26.998945 c +0.176406 13.224656 l +-0.277692 12.560975 0.197552 11.659972 1.001714 11.659972 c +5.146065 11.659972 l +5.760547 11.659972 6.229665 11.110973 6.133829 10.504009 c +4.797663 2.041628 l +4.631372 0.988457 6.008657 0.441025 6.610733 1.320982 c +16.035248 15.095271 l +16.489344 15.758952 16.014101 16.659954 15.209939 16.659954 c +11.065588 16.659954 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 637 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 30.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 +0000000727 00000 n +0000000749 00000 n +0000000922 00000 n +0000000996 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1055 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json new file mode 100644 index 0000000000..675ff0268b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Boost.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json new file mode 100644 index 0000000000..45508776ad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SmallBoost.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf new file mode 100644 index 0000000000..fd5f51667d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf @@ -0,0 +1,79 @@ +%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 1.259766 0.561462 cm +1.000000 1.000000 1.000000 scn +7.377057 11.106611 m +6.967402 11.106611 6.654656 11.472610 6.718547 11.877253 c +7.609325 17.518847 l +7.720185 18.220961 6.801996 18.585918 6.400612 17.999279 c +0.117604 8.816422 l +-0.185128 8.373968 0.131700 7.773299 0.667809 7.773299 c +3.430712 7.773299 l +3.840366 7.773299 4.153112 7.407299 4.089221 7.002657 c +3.198443 1.361063 l +3.087583 0.658949 4.005772 0.293991 4.407156 0.880630 c +10.690165 10.063488 l +10.992896 10.505941 10.676067 11.106611 10.139959 11.106611 c +7.377057 11.106611 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 622 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 13.333374 20.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 +0000000712 00000 n +0000000734 00000 n +0000000907 00000 n +0000000981 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1040 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json new file mode 100644 index 0000000000..4553569f18 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Copy.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf new file mode 100644 index 0000000000..ba20ebd885 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf @@ -0,0 +1,402 @@ +%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 9.000000 7.000000 cm +1.000000 1.000000 1.000000 scn +1.092019 13.782013 m +1.519843 14.000000 2.079895 14.000000 3.200000 14.000000 c +3.674516 14.000000 l +4.163698 14.000000 4.408288 14.000000 4.638463 13.944740 c +4.842536 13.895746 5.037625 13.814938 5.216570 13.705280 c +5.418403 13.581597 5.591354 13.408646 5.937256 13.062744 c +5.937259 13.062741 l +10.062741 8.937258 l +10.408645 8.591354 10.581596 8.418404 10.705280 8.216570 c +10.814938 8.037625 10.895746 7.842536 10.944740 7.638463 c +11.000000 7.408288 11.000000 7.163698 11.000000 6.674517 c +11.000000 3.200000 l +11.000000 2.079895 11.000000 1.519842 10.782013 1.092019 c +10.590266 0.715694 10.284306 0.409734 9.907981 0.217987 c +9.480158 0.000000 8.920105 0.000000 7.800001 0.000000 c +3.200000 0.000000 l +2.079895 0.000000 1.519843 0.000000 1.092019 0.217987 c +0.715695 0.409734 0.409734 0.715694 0.217987 1.092019 c +0.000000 1.519842 0.000000 2.079895 0.000000 3.200000 c +0.000000 10.800000 l +0.000000 11.920105 0.000000 12.480158 0.217987 12.907981 c +0.409734 13.284306 0.715695 13.590266 1.092019 13.782013 c +h +5.000000 11.896446 m +5.000000 9.000000 l +5.000000 8.447716 5.447715 8.000000 6.000000 8.000000 c +8.896446 8.000000 l +9.119173 8.000000 9.230715 8.269285 9.073223 8.426777 c +5.426776 12.073224 l +5.269285 12.230715 5.000000 12.119173 5.000000 11.896446 c +h +f* +n +Q +q +q +1.000000 0.000000 -0.000000 1.000000 4.000000 0.330414 cm +0.529412 0.490196 1.000000 scn +1.092019 16.451599 m +0.488212 17.636639 l +0.488212 17.636639 l +1.092019 16.451599 l +h +4.638463 16.614326 m +4.327981 15.321074 l +4.327981 15.321074 l +4.638463 16.614326 l +h +5.216570 16.374866 m +4.521646 15.240855 l +4.521647 15.240855 l +5.216570 16.374866 l +h +5.937256 15.732330 m +6.931310 16.615934 l +6.914003 16.635405 6.896128 16.654364 6.877707 16.672783 c +5.937256 15.732330 l +h +5.937259 15.732327 m +4.943204 14.848723 l +4.960511 14.829253 4.978386 14.810296 4.996807 14.791876 c +5.937259 15.732327 l +h +10.062741 11.606844 m +9.122289 10.666392 l +9.122290 10.666392 l +10.062741 11.606844 l +h +10.705280 10.886156 m +11.839292 11.581079 l +11.839291 11.581080 l +10.705280 10.886156 l +h +10.944740 10.308050 m +12.237993 10.618530 l +12.237991 10.618536 l +10.944740 10.308050 l +h +10.782013 3.761605 m +9.596974 4.365414 l +9.596973 4.365412 l +10.782013 3.761605 l +h +9.907981 2.887573 m +9.304174 4.072613 l +9.304173 4.072612 l +9.907981 2.887573 l +h +1.092019 2.887573 m +1.695827 4.072612 l +1.695826 4.072612 l +1.092019 2.887573 l +h +0.217987 3.761605 m +1.403026 4.365412 l +1.403025 4.365413 l +0.217987 3.761605 l +h +0.217987 15.577567 m +-0.967052 16.181376 l +-0.967052 16.181376 l +0.217987 15.577567 l +h +9.073223 11.096363 m +8.132771 10.155910 l +8.132772 10.155910 l +9.073223 11.096363 l +h +5.426776 14.742810 m +6.367229 15.683262 l +6.367229 15.683262 l +5.426776 14.742810 l +h +3.200000 17.999586 m +2.661894 17.999586 2.178326 18.000620 1.778100 17.967920 c +1.362347 17.933952 0.920866 17.857086 0.488212 17.636639 c +1.695826 15.266561 l +1.696496 15.266901 1.699535 15.268437 1.706597 15.270958 c +1.713946 15.273581 1.727419 15.277899 1.749235 15.283036 c +1.795082 15.293834 1.870400 15.306599 1.994708 15.316755 c +2.261491 15.338552 2.618001 15.339586 3.200000 15.339586 c +3.200000 17.999586 l +h +3.674516 17.999586 m +3.200000 17.999586 l +3.200000 15.339586 l +3.674516 15.339586 l +3.674516 17.999586 l +h +4.948946 17.907578 m +4.530101 18.008133 4.100392 17.999586 3.674516 17.999586 c +3.674516 15.339586 l +4.227004 15.339586 4.286476 15.331038 4.327981 15.321074 c +4.948946 17.907578 l +h +5.911493 17.508879 m +5.613550 17.691458 5.288726 17.826004 4.948946 17.907578 c +4.327981 15.321074 l +4.396346 15.304661 4.461700 15.277590 4.521646 15.240855 c +5.911493 17.508879 l +h +6.877707 16.672783 m +6.576569 16.973921 6.278763 17.283815 5.911493 17.508879 c +4.521647 15.240855 l +4.558042 15.218552 4.606139 15.182544 4.996805 14.791878 c +6.877707 16.672783 l +h +6.931313 16.615932 m +6.931310 16.615934 l +4.943202 14.848727 l +4.943204 14.848723 l +6.931313 16.615932 l +h +11.003194 12.547297 m +6.877711 16.672779 l +4.996807 14.791876 l +9.122289 10.666392 l +11.003194 12.547297 l +h +11.839291 11.581080 m +11.614226 11.948351 11.304334 12.246157 11.003193 12.547297 c +9.122290 10.666392 l +9.512956 10.275725 9.548966 10.227628 9.571270 10.191232 c +11.839291 11.581080 l +h +12.237991 10.618536 m +12.156417 10.958313 12.021872 11.283136 11.839292 11.581079 c +9.571269 10.191234 l +9.608004 10.131287 9.635076 10.065931 9.651489 9.997564 c +12.237991 10.618536 l +h +12.330000 9.344103 m +12.330000 9.769979 12.338548 10.199686 12.237993 10.618530 c +9.651487 9.997570 l +9.661452 9.956062 9.670000 9.896589 9.670000 9.344103 c +12.330000 9.344103 l +h +12.330000 5.869586 m +12.330000 9.344103 l +9.670000 9.344103 l +9.670000 5.869586 l +12.330000 5.869586 l +h +11.967052 3.157797 m +12.187500 3.590451 12.264366 4.031933 12.298335 4.447685 c +12.331035 4.847912 12.330000 5.331480 12.330000 5.869586 c +9.670000 5.869586 l +9.670000 5.287587 9.668965 4.931077 9.647168 4.664294 c +9.637012 4.539987 9.624248 4.464668 9.613450 4.418821 c +9.608313 4.397006 9.603994 4.383533 9.601372 4.376184 c +9.598851 4.369123 9.597316 4.366082 9.596974 4.365414 c +11.967052 3.157797 l +h +10.511787 1.702534 m +11.138369 2.021792 11.647794 2.531218 11.967052 3.157799 c +9.596973 4.365412 l +9.532739 4.239344 9.430243 4.136847 9.304174 4.072613 c +10.511787 1.702534 l +h +7.800001 1.339586 m +8.338107 1.339586 8.821674 1.338552 9.221901 1.371251 c +9.637653 1.405220 10.079135 1.482086 10.511789 1.702535 c +9.304173 4.072612 l +9.303504 4.072270 9.300464 4.070735 9.293403 4.068214 c +9.286054 4.065592 9.272580 4.061274 9.250765 4.056136 c +9.204918 4.045339 9.129600 4.032574 9.005292 4.022418 c +8.738509 4.000621 8.381999 3.999586 7.800001 3.999586 c +7.800001 1.339586 l +h +3.200000 1.339586 m +7.800001 1.339586 l +7.800001 3.999586 l +3.200000 3.999586 l +3.200000 1.339586 l +h +0.488211 1.702535 m +0.920865 1.482086 1.362347 1.405220 1.778099 1.371251 c +2.178326 1.338552 2.661894 1.339586 3.200000 1.339586 c +3.200000 3.999586 l +2.618001 3.999586 2.261491 4.000621 1.994708 4.022418 c +1.870400 4.032574 1.795082 4.045339 1.749235 4.056136 c +1.727419 4.061274 1.713946 4.065592 1.706597 4.068214 c +1.699536 4.070735 1.696496 4.072271 1.695827 4.072612 c +0.488211 1.702535 l +h +-0.967052 3.157799 m +-0.647793 2.531218 -0.138368 2.021792 0.488212 1.702535 c +1.695826 4.072612 l +1.569757 4.136847 1.467261 4.239344 1.403026 4.365412 c +-0.967052 3.157799 l +h +-1.330000 5.869586 m +-1.330000 5.331480 -1.331034 4.847912 -1.298335 4.447685 c +-1.264366 4.031933 -1.187500 3.590451 -0.967052 3.157798 c +1.403025 4.365413 l +1.402685 4.366082 1.401149 4.369122 1.398628 4.376184 c +1.396005 4.383532 1.391687 4.397006 1.386550 4.418821 c +1.375753 4.464668 1.362988 4.539987 1.352831 4.664294 c +1.331034 4.931077 1.330000 5.287587 1.330000 5.869586 c +-1.330000 5.869586 l +h +-1.330000 13.469586 m +-1.330000 5.869586 l +1.330000 5.869586 l +1.330000 13.469586 l +-1.330000 13.469586 l +h +-0.967052 16.181376 m +-1.187500 15.748720 -1.264366 15.307240 -1.298335 14.891487 c +-1.331034 14.491261 -1.330000 14.007692 -1.330000 13.469586 c +1.330000 13.469586 l +1.330000 14.051584 1.331034 14.408095 1.352831 14.674878 c +1.362988 14.799186 1.375753 14.874504 1.386550 14.920351 c +1.391687 14.942167 1.396005 14.955641 1.398629 14.962990 c +1.401149 14.970051 1.402685 14.973091 1.403026 14.973760 c +-0.967052 16.181376 l +h +0.488212 17.636639 m +-0.138368 17.317379 -0.647793 16.807955 -0.967052 16.181376 c +1.403026 14.973760 l +1.467261 15.099829 1.569758 15.202326 1.695826 15.266561 c +0.488212 17.636639 l +h +6.330000 11.669586 m +6.330000 14.566032 l +3.670000 14.566032 l +3.670000 11.669586 l +6.330000 11.669586 l +h +6.000000 11.999586 m +6.182254 11.999586 6.330000 11.851840 6.330000 11.669586 c +3.670000 11.669586 l +3.670000 10.382763 4.713177 9.339586 6.000000 9.339586 c +6.000000 11.999586 l +h +8.896446 11.999586 m +6.000000 11.999586 l +6.000000 9.339586 l +8.896446 9.339586 l +8.896446 11.999586 l +h +8.132772 10.155910 m +7.452406 10.836273 7.934273 11.999586 8.896446 11.999586 c +8.896446 9.339586 l +10.304073 9.339586 11.009024 11.041470 10.013674 12.036817 c +8.132772 10.155910 l +h +4.486324 13.802358 m +8.132771 10.155910 l +10.013675 12.036816 l +6.367229 15.683262 l +4.486324 13.802358 l +h +6.330000 14.566032 m +6.330000 13.603863 5.166691 13.121991 4.486324 13.802358 c +6.367229 15.683262 l +5.371879 16.678612 3.670000 15.973656 3.670000 14.566032 c +6.330000 14.566032 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 4.000000 0.330414 cm +1.000000 1.000000 1.000000 scn +1.092019 16.451599 m +1.519843 16.669586 2.079895 16.669586 3.200000 16.669586 c +3.674516 16.669586 l +4.163698 16.669586 4.408288 16.669586 4.638463 16.614326 c +4.842536 16.565332 5.037625 16.484524 5.216570 16.374866 c +5.418403 16.251183 5.591354 16.078232 5.937256 15.732330 c +5.937259 15.732327 l +10.062741 11.606844 l +10.408645 11.260941 10.581596 11.087990 10.705280 10.886156 c +10.814938 10.707211 10.895746 10.512122 10.944740 10.308050 c +11.000000 10.077875 11.000000 9.833284 11.000000 9.344103 c +11.000000 5.869586 l +11.000000 4.749481 11.000000 4.189428 10.782013 3.761605 c +10.590266 3.385281 10.284306 3.079320 9.907981 2.887573 c +9.480158 2.669586 8.920105 2.669586 7.800001 2.669586 c +3.200000 2.669586 l +2.079895 2.669586 1.519843 2.669586 1.092019 2.887573 c +0.715695 3.079320 0.409734 3.385281 0.217987 3.761605 c +0.000000 4.189428 0.000000 4.749481 0.000000 5.869586 c +0.000000 13.469586 l +0.000000 14.589691 0.000000 15.149744 0.217987 15.577567 c +0.409734 15.953892 0.715695 16.259853 1.092019 16.451599 c +h +5.000000 14.566032 m +5.000000 11.669586 l +5.000000 11.117302 5.447715 10.669586 6.000000 10.669586 c +8.896446 10.669586 l +9.119173 10.669586 9.230715 10.938871 9.073223 11.096363 c +5.426776 14.742810 l +5.269285 14.900301 5.000000 14.788759 5.000000 14.566032 c +h +f* +n +Q +Q + +endstream +endobj + +3 0 obj + 9865 +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 +0000009955 00000 n +0000009978 00000 n +0000010151 00000 n +0000010225 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +10284 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 941637ea07..2b4d85bf51 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4168,7 +4168,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.dismissInput() let botName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - + let botAddress = peer.addressName ?? "" + if source == .generic { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { @@ -4222,6 +4223,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: true, fromAttachMenu: false, isInline: false, isSimple: false) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + if let strongSelf = self { + if let chatTypes { + let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + controller.peerSelected = { [weak self, weak controller] peer, _ in + if let strongSelf = self { + completion() + controller?.dismiss() + strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) + } + } + strongSelf.push(controller) + } else { + strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + } + } }, getInputContainerNode: { [weak self] in if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass { return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 130a758930..dc621a2614 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -80,9 +80,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private var automaticDownload: Bool? var media: TelegramMediaFile? var appliedForwardInfo: (Peer?, String?)? - - private var secretProgressIcon: UIImage? - + private let fetchDisposable = MetaDisposable() private var durationBackgroundNode: NavigationBackgroundNode? @@ -259,9 +257,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let theme = item.presentationData.theme let isSecretMedia = item.message.containsSecretMedia - var secretProgressIcon: UIImage? if isSecretMedia { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme.theme) secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty) } @@ -575,7 +571,6 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { strongSelf.item = item strongSelf.videoFrame = displayVideoFrame strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) - strongSelf.secretProgressIcon = secretProgressIcon strongSelf.automaticDownload = automaticDownload @@ -1155,7 +1150,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true) } case .Local: - if isSecretMedia && self.secretProgressIcon != nil { + if isSecretMedia { if let (beginTime, timeout) = secretBeginTimeAndTimeout { state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) } else { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 6dfa7bd625..803997ee00 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -24,6 +24,7 @@ import ChatMessageInteractiveMediaBadge import ContextUI import InvisibleInkDustNode import ChatControllerInteraction +import StoryContainerScreen private struct FetchControls { let fetch: (Bool) -> Void @@ -866,9 +867,6 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { maxWidth = maxDimensions.width } - if isSecretMedia { - let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(presentationData.theme.theme) - } return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in var resultWidth: CGFloat @@ -940,7 +938,16 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if !mediaUpdated, let media = media as? TelegramMediaStory { if message.associatedStories[media.storyId] != currentMessage?.associatedStories[media.storyId] { - mediaUpdated = true + let previousStory = message.associatedStories[media.storyId] + let updatedStory = currentMessage?.associatedStories[media.storyId] + + if let previousItem = previousStory?.get(Stories.StoredItem.self), let updatedItem = updatedStory?.get(Stories.StoredItem.self), case let .item(previousItemValue) = previousItem, case let .item(updatedItemValue) = updatedItem { + if let previousItemMedia = previousItemValue.media, let updatedItemMedia = updatedItemValue.media { + mediaUpdated = !previousItemMedia.isSemanticallyEqual(to: updatedItemMedia) + } + } else { + mediaUpdated = true + } } } } else { @@ -1037,7 +1044,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } else { updateImageSignal = { synchronousLoad, highQuality in - return chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) + return storyPreviewWithAddedReactions(context: context, storyItem: item, signal: chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality)) } updateBlurredImageSignal = { synchronousLoad, _ in return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true, synchronousLoad: true) @@ -1074,7 +1081,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { onlyFullSizeVideoThumbnail = isSendingUpdated updateImageSignal = { synchronousLoad, _ in - return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + return storyPreviewWithAddedReactions(context: context, storyItem: item, signal: mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true)) } updateBlurredImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) @@ -1151,7 +1158,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if isSecretMedia { updateImageSignal = { synchronousLoad, _ in - return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true) } } else { updateImageSignal = { synchronousLoad, highQuality in @@ -1986,15 +1993,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } case .Local: state = .none - let secretProgressIcon: UIImage? - if case .constrained = sizeCalculation { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) - } else { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme) - } if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime, Int32(timeout) != viewOnceTimeout { state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) - } else if isSecretMedia, let _ = secretProgressIcon { + } else if isSecretMedia { state = .staticTimeout } else if let file = media as? TelegramMediaFile, !file.isVideoSticker { let isInlinePlayableVideo = file.isVideo && !isSecretMedia && (self.automaticPlayback ?? false) diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index cb25975c33..40c9847038 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -111,6 +111,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private let text: TooltipScreen.Text private let textAlignment: TooltipScreen.Alignment private let balancedTextLayout: Bool + private let constrainWidth: CGFloat? private let tooltipStyle: TooltipScreen.Style private let arrowStyle: TooltipScreen.ArrowStyle private let icon: TooltipScreen.Icon? @@ -159,6 +160,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment, balancedTextLayout: Bool, + constrainWidth: CGFloat?, style: TooltipScreen.Style, arrowStyle: TooltipScreen.ArrowStyle, icon: TooltipScreen.Icon? = nil, @@ -379,6 +381,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.text = text self.textAlignment = textAlignment self.balancedTextLayout = balancedTextLayout + self.constrainWidth = constrainWidth self.animatedStickerNode = DefaultAnimatedStickerNodeImpl() switch icon { @@ -491,7 +494,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { animationSpacing = 8.0 } - let containerWidth = max(100.0, min(layout.size.width - sideInset * 2.0, 614.0)) + var containerWidth = max(100.0, min(layout.size.width - sideInset * 2.0, 614.0)) + if let constrainWidth = self.constrainWidth, constrainWidth > 100.0 { + containerWidth = constrainWidth + } var actionSize: CGSize = .zero @@ -1001,6 +1007,7 @@ public final class TooltipScreen: ViewController { public let text: TooltipScreen.Text public let textAlignment: TooltipScreen.Alignment private let balancedTextLayout: Bool + private let constrainWidth: CGFloat? private let style: TooltipScreen.Style private let arrowStyle: TooltipScreen.ArrowStyle private let icon: TooltipScreen.Icon? @@ -1039,6 +1046,7 @@ public final class TooltipScreen: ViewController { text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment = .natural, balancedTextLayout: Bool = false, + constrainWidth: CGFloat? = nil, style: TooltipScreen.Style = .default, arrowStyle: TooltipScreen.ArrowStyle = .default, icon: TooltipScreen.Icon? = nil, @@ -1056,6 +1064,7 @@ public final class TooltipScreen: ViewController { self.text = text self.textAlignment = textAlignment self.balancedTextLayout = balancedTextLayout + self.constrainWidth = constrainWidth self.style = style self.arrowStyle = arrowStyle self.icon = icon @@ -1123,7 +1132,7 @@ public final class TooltipScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, constrainWidth: self.constrainWidth, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return }