diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index cbc9466f9c..b3e34d4cd2 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -173,8 +173,11 @@ private final class CameraContext { self.positionValue = configuration.position self._positionPromise = ValuePromise(configuration.position) +#if targetEnvironment(simulator) +#else self.setDualCameraEnabled(configuration.isDualEnabled, change: false) - +#endif + NotificationCenter.default.addObserver( self, selector: #selector(self.sessionRuntimeError), diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 7e61cbe1d5..e95713a3a3 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -67,6 +67,7 @@ public final class SheetComponent: Component { public let externalState: ExternalState? public let animateOut: ActionSlot> public let onPan: () -> Void + public let willDismiss: () -> Void public init( content: AnyComponent, @@ -76,7 +77,8 @@ public final class SheetComponent: Component { isScrollEnabled: Bool = true, externalState: ExternalState? = nil, animateOut: ActionSlot>, - onPan: @escaping () -> Void = {} + onPan: @escaping () -> Void = {}, + willDismiss: @escaping () -> Void = {} ) { self.content = content self.backgroundColor = backgroundColor @@ -86,6 +88,7 @@ public final class SheetComponent: Component { self.externalState = externalState self.animateOut = animateOut self.onPan = onPan + self.willDismiss = willDismiss } public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool { @@ -222,6 +225,7 @@ public final class SheetComponent: Component { let currentContentOffset = scrollView.contentOffset targetContentOffset.pointee = currentContentOffset if velocity.y > 300.0 { + self.component?.willDismiss() self.animateOut(initialVelocity: initialVelocity, completion: { self.dismiss?(false) }) @@ -233,6 +237,7 @@ public final class SheetComponent: Component { scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true) } } else { + self.component?.willDismiss() self.animateOut(initialVelocity: initialVelocity, completion: { self.dismiss?(false) }) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index fd0edc75e8..a3ffedec81 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -467,6 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[583071445] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) } dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) } dict[-251549057] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftChat($0) } + dict[545636920] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftSlug($0) } dict[1764202389] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftUser($0) } dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) } dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) } diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index a01a8cf8bd..6057c17d36 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -367,6 +367,7 @@ public extension Api { public extension Api { indirect enum InputSavedStarGift: TypeConstructorDescription { case inputSavedStarGiftChat(peer: Api.InputPeer, savedId: Int64) + case inputSavedStarGiftSlug(slug: String) case inputSavedStarGiftUser(msgId: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -378,6 +379,12 @@ public extension Api { peer.serialize(buffer, true) serializeInt64(savedId, buffer: buffer, boxed: false) break + case .inputSavedStarGiftSlug(let slug): + if boxed { + buffer.appendInt32(545636920) + } + serializeString(slug, buffer: buffer, boxed: false) + break case .inputSavedStarGiftUser(let msgId): if boxed { buffer.appendInt32(1764202389) @@ -391,6 +398,8 @@ public extension Api { switch self { case .inputSavedStarGiftChat(let peer, let savedId): return ("inputSavedStarGiftChat", [("peer", peer as Any), ("savedId", savedId as Any)]) + case .inputSavedStarGiftSlug(let slug): + return ("inputSavedStarGiftSlug", [("slug", slug as Any)]) case .inputSavedStarGiftUser(let msgId): return ("inputSavedStarGiftUser", [("msgId", msgId as Any)]) } @@ -412,6 +421,17 @@ public extension Api { return nil } } + public static func parse_inputSavedStarGiftSlug(_ reader: BufferReader) -> InputSavedStarGift? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputSavedStarGift.inputSavedStarGiftSlug(slug: _1!) + } + else { + return nil + } + } public static func parse_inputSavedStarGiftUser(_ reader: BufferReader) -> InputSavedStarGift? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 8f39ceae97..35b5eecc84 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -9791,12 +9791,12 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func updateStarGiftPrice(slug: String, resellStars: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func updateStarGiftPrice(stargift: Api.InputSavedStarGift, resellStars: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-489360582) - serializeString(slug, buffer: buffer, boxed: false) + buffer.appendInt32(1001301217) + stargift.serialize(buffer, true) serializeInt64(resellStars, buffer: buffer, boxed: false) - return (FunctionDescription(name: "payments.updateStarGiftPrice", parameters: [("slug", String(describing: slug)), ("resellStars", String(describing: resellStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "payments.updateStarGiftPrice", parameters: [("stargift", String(describing: stargift)), ("resellStars", String(describing: resellStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 3425c1f487..73c0fffe67 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1509,14 +1509,14 @@ private final class ProfileGiftsContextImpl { } } - func updateStarGiftResellPrice(slug: String, price: Int64?) { + func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) { self.actionDisposable.set( - _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price).startStrict() + _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price).startStrict() ) if let index = self.gifts.firstIndex(where: { gift in - if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { + if gift.reference == reference { return true } return false @@ -1529,7 +1529,7 @@ private final class ProfileGiftsContextImpl { } if let index = self.filteredGifts.firstIndex(where: { gift in - if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { + if gift.reference == reference { return true } return false @@ -1939,9 +1939,9 @@ public final class ProfileGiftsContext { } } - public func updateStarGiftResellPrice(slug: String, price: Int64?) { + public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) { self.impl.with { impl in - impl.updateStarGiftResellPrice(slug: slug, price: price) + impl.updateStarGiftResellPrice(reference: reference, price: price) } } @@ -2082,10 +2082,12 @@ public enum StarGiftReference: Equatable, Hashable, Codable { case messageId case peerId case id + case slug } case message(messageId: EngineMessage.Id) case peer(peerId: EnginePeer.Id, id: Int64) + case slug(slug: String) public enum DecodingError: Error { case generic @@ -2100,6 +2102,8 @@ public enum StarGiftReference: Equatable, Hashable, Codable { self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId)) case 1: self = .peer(peerId: try container.decode(EnginePeer.Id.self, forKey: .peerId), id: try container.decode(Int64.self, forKey: .id)) + case 2: + self = .slug(slug: try container.decode(String.self, forKey: .slug)) default: throw DecodingError.generic } @@ -2116,6 +2120,9 @@ public enum StarGiftReference: Equatable, Hashable, Codable { try container.encode(1 as Int32, forKey: .type) try container.encode(peerId, forKey: .peerId) try container.encode(id, forKey: .id) + case let .slug(slug): + try container.encode(2 as Int32, forKey: .type) + try container.encode(slug, forKey: .slug) } } } @@ -2130,6 +2137,8 @@ extension StarGiftReference { return nil } return .inputSavedStarGiftChat(peer: inputPeer, savedId: id) + case let .slug(slug): + return .inputSavedStarGiftSlug(slug: slug) } } } @@ -2265,19 +2274,27 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer } } -func _internal_updateStarGiftResalePrice(account: Account, slug: String, price: Int64?) -> Signal { - return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) +func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { + return account.postbox.transaction { transaction in + return reference.apiStarGiftReference(transaction: transaction) } - |> mapToSignal { updates -> Signal in - if let updates { - account.stateManager.addUpdates(updates) + |> mapToSignal { starGift in + guard let starGift else { + return .complete() } - return .complete() + return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates { + account.stateManager.addUpdates(updates) + } + return .complete() + } + |> ignoreValues } - |> ignoreValues } public extension StarGift.UniqueGift { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 4677f08a9e..87b914ce58 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -153,8 +153,8 @@ public extension TelegramEngine { return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled) } - public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal { - return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price) + public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { + return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) } } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 379755dd6b..001fa6f4fe 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -866,7 +866,8 @@ public final class GiftItemComponent: Component { return (TelegramTextAttributes.URL, contents) } ) - let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(resellPrice)", attributes: attributes)) + let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat + let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes)) if let range = labelText.string.range(of: "#") { labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string)) labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 76e2d6e3fa..cdc4322ae2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -465,7 +465,6 @@ final class GiftSetupScreenComponent: Component { self.inProgress = false self.state?.updated() - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } var errorText: String? switch error { case .starGiftOutOfStock: diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 46fee26be0..6023e86c6f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -72,7 +72,7 @@ final class GiftStoreScreenComponent: Component { private let loadingNode: LoadingShimmerNode private let emptyResultsAnimation = ComponentView() private let emptyResultsTitle = ComponentView() - private let emptyResultsAction = ComponentView() + private let clearFilters = ComponentView() private let topPanel = ComponentView() private let topSeparator = ComponentView() @@ -139,10 +139,21 @@ final class GiftStoreScreenComponent: Component { self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) } + private var removedStarGifts = Set() private var currentGifts: ([StarGift], Set, Set, Set)? private var effectiveGifts: [StarGift]? { if let gifts = self.state?.starGiftsState?.gifts { - return gifts + if !self.removedStarGifts.isEmpty { + return gifts.filter { gift in + if case let .unique(uniqueGift) = gift { + return !self.removedStarGifts.contains(uniqueGift.slug) + } else { + return true + } + } + } else { + return gifts + } } else { return nil } @@ -154,6 +165,7 @@ final class GiftStoreScreenComponent: Component { } let availableWidth = self.scrollView.bounds.width + let availableHeight = self.scrollView.bounds.height let contentOffset = self.scrollView.contentOffset.y let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0 @@ -213,8 +225,8 @@ final class GiftStoreScreenComponent: Component { font: .monospaced, color: ribbonColor ) - - let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(uniqueGift.resellStars ?? 0)") + + let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))") let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -243,6 +255,13 @@ final class GiftStoreScreenComponent: Component { context: component.context, subject: .uniqueGift(uniqueGift, state.peerId) ) + giftController.onBuySuccess = { [weak self] in + guard let self else { + return + } + self.removedStarGifts.insert(uniqueGift.slug) + self.state?.updated(transition: .spring(duration: 0.3)) + } mainController.push(giftController) } } @@ -288,6 +307,138 @@ final class GiftStoreScreenComponent: Component { } } + let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) + let emptyResultsActionSize = self.clearFilters.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ) + ), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + self.state?.starGiftsContext.updateFilterAttributes([]) + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableWidth - 44.0 * 2.0, height: 100.0) + ) + + var showClearFilters = false + if let filterAttributes = self.state?.starGiftsState?.filterAttributes, !filterAttributes.isEmpty { + showClearFilters = true + } + + let topInset: CGFloat = environment.navigationHeight + 39.0 + let bottomInset: CGFloat = environment.safeInsets.bottom + + var emptyResultsActionFrame = CGRect( + origin: CGPoint( + x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), + y: max(self.scrollView.contentSize.height - 8.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0) + ), + size: emptyResultsActionSize + ) + + if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading { + showClearFilters = true + + let emptyAnimationHeight = 148.0 + let visibleHeight = availableHeight + let emptyAnimationSpacing: CGFloat = 20.0 + let emptyTextSpacing: CGFloat = 18.0 + + let emptyResultsTitleSize = self.emptyResultsTitle.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + + let emptyResultsAnimationSize = self.emptyResultsAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "ChatListNoResults") + )), + environment: {}, + containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight) + ) + + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + + let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize) + + let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize) + + emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize) + + if let view = self.emptyResultsAnimation.view as? LottieComponent.View { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.insertSubview(view, belowSubview: self.loadingNode.view) + view.playOnce() + } + view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) + ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center) + } + if let view = self.emptyResultsTitle.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.insertSubview(view, belowSubview: self.loadingNode.view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) + ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center) + } + } else { + if let view = self.emptyResultsAnimation.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsTitle.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + } + + if showClearFilters { + if let view = self.clearFilters.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.insertSubview(view, belowSubview: self.loadingNode.view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) + ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) + + view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0 + } + } else { + if let view = self.clearFilters.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + } + let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) if interactive, bottomContentOffset < 320.0 { self.state?.starGiftsContext.loadMore() @@ -966,118 +1117,7 @@ final class GiftStoreScreenComponent: Component { loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0) } transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize)) - - let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) - if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading { - let sideInset: CGFloat = 44.0 - let emptyAnimationHeight = 148.0 - let topInset: CGFloat = environment.navigationHeight + 39.0 - let bottomInset: CGFloat = environment.safeInsets.bottom - let visibleHeight = availableSize.height - let emptyAnimationSpacing: CGFloat = 20.0 - let emptyTextSpacing: CGFloat = 18.0 - - let emptyResultsTitleSize = self.emptyResultsTitle.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center - ) - ), - environment: {}, - containerSize: availableSize - ) - let emptyResultsActionSize = self.emptyResultsAction.update( - transition: .immediate, - component: AnyComponent( - PlainButtonComponent( - content: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: theme.list.itemAccentColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - ) - ), - effectAlignment: .center, - action: { [weak self] in - guard let self else { - return - } - self.state?.starGiftsContext.updateFilterAttributes([]) - }, - animateScale: false - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: visibleHeight) - ) - let emptyResultsAnimationSize = self.emptyResultsAnimation.update( - transition: .immediate, - component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "ChatListNoResults") - )), - environment: {}, - containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight) - ) - - let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing - let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) - let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize) - - let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize) - - let emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize) - - if let view = self.emptyResultsAnimation.view as? LottieComponent.View { - if view.superview == nil { - view.alpha = 0.0 - fadeTransition.setAlpha(view: view, alpha: 1.0) - self.insertSubview(view, belowSubview: self.loadingNode.view) - view.playOnce() - } - view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) - ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center) - } - if let view = self.emptyResultsTitle.view { - if view.superview == nil { - view.alpha = 0.0 - fadeTransition.setAlpha(view: view, alpha: 1.0) - self.insertSubview(view, belowSubview: self.loadingNode.view) - } - view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) - ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center) - } - if let view = self.emptyResultsAction.view { - if view.superview == nil { - view.alpha = 0.0 - fadeTransition.setAlpha(view: view, alpha: 1.0) - self.insertSubview(view, belowSubview: self.loadingNode.view) - } - view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) - ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) - - view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0 - } - } else { - if let view = self.emptyResultsAnimation.view { - fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in - view.removeFromSuperview() - }) - } - if let view = self.emptyResultsTitle.view { - fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in - view.removeFromSuperview() - }) - } - if let view = self.emptyResultsAction.view { - fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in - view.removeFromSuperview() - }) - } - } - return availableSize } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift index 12336824b9..ac59b7a2ce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift @@ -10,10 +10,10 @@ import AccountContext import TelegramPresentationData final class PriceButtonComponent: Component { - let price: Int64 + let price: String init( - price: Int64 + price: String ) { self.price = price } @@ -54,7 +54,7 @@ final class PriceButtonComponent: Component { transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "\(component.price)", + string: component.price, font: Font.semibold(11.0), textColor: UIColor(rgb: 0xffffff) )) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 3125cacc51..76bf4b098c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -444,10 +444,24 @@ private final class GiftViewSheetContent: CombinedComponent { self.updated() self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId) - |> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in + |> deliverOnMainQueue).start(error: { [weak self] error in + guard let self, let controller = self.getController() else { + return + } + + self.inProgress = false + self.updated() + + let errorText = presentationData.strings.Gift_Send_ErrorUnknown + + let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) + controller.present(alertController, in: .window(.root)) + }, completed: { [weak self, weak starsContext] in guard let self, let controller = self.getController() as? GiftViewScreen else { return } + controller.onBuySuccess() + self.inProgress = false var animationFile: TelegramMediaFile? @@ -459,41 +473,26 @@ private final class GiftViewSheetContent: CombinedComponent { } if let navigationController = controller.navigationController as? NavigationController { - if recipientPeerId == self.context.account.peerId { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak navigationController] peer in - guard let peer, let navigationController else { - return + if recipientPeerId == self.context.account.peerId { + var controllers = navigationController.viewControllers + controllers = controllers.filter({ !($0 is GiftViewScreen) }) + navigationController.setViewControllers(controllers, animated: true) + + //TODO:localize + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + + Queue.mainQueue().after(0.5, { + if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { _ in + return true + } + ) + lastController.present(resultController, in: .window(.root)) } - - var controllers = Array(navigationController.viewControllers.prefix(1)) - if let controller = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: .myProfileGifts, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - controllers.append(controller) - } - navigationController.setViewControllers(controllers, animated: true) - navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) - - Queue.mainQueue().after(0.5, { - if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil), - elevatedLayout: lastController is ChatController, - action: { _ in - return true - } - ) - lastController.present(resultController, in: .window(.root)) - } - }) }) } else { var controllers = Array(navigationController.viewControllers.prefix(1)) @@ -884,17 +883,16 @@ private final class GiftViewSheetContent: CombinedComponent { headerSubject = nil } - var ownerPeerId: EnginePeer.Id + var ownerPeerId: EnginePeer.Id? if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner { ownerPeerId = peerId - } else { - ownerPeerId = component.context.account.peerId } + let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId var wearPeerNameChild: _UpdatedChildComponent? if showWearPreview, let uniqueGift { var peerName = "" - if let ownerPeer = state.peerMap[ownerPeerId] { + if let ownerPeer = state.peerMap[wearOwnerPeerId] { peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) } wearPeerNameChild = wearPeerName.update( @@ -1004,7 +1002,7 @@ private final class GiftViewSheetContent: CombinedComponent { } if let wearPeerNameChild { - if let ownerPeer = state.peerMap[ownerPeerId] { + if let ownerPeer = state.peerMap[wearOwnerPeerId] { let wearAvatar = wearAvatar.update( component: AvatarComponent( context: component.context, @@ -1488,8 +1486,7 @@ private final class GiftViewSheetContent: CombinedComponent { if !soldOut { if let uniqueGift { - if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId { - //TODO:localize + if !"".isEmpty, case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId { if let peer = state.peerMap[recipientPeerId] { tableItems.append(.init( id: "recipient", @@ -1815,7 +1812,7 @@ private final class GiftViewSheetContent: CombinedComponent { } let canWear: Bool - if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId] { + if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId] { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration)) if let boostLevel = channel.approximateBoostLevel { @@ -2232,29 +2229,28 @@ private final class GiftViewSheetContent: CombinedComponent { if let uniqueGift { resellStars = uniqueGift.resellStars - if incoming, let resellStars { - let priceButton = priceButton.update( - component: PlainButtonComponent( - content: AnyComponent( - PriceButtonComponent(price: resellStars) + if let resellStars { + if incoming || ownerPeerId == component.context.account.peerId { + let priceButton = priceButton.update( + component: PlainButtonComponent( + content: AnyComponent( + PriceButtonComponent(price: presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator)) + ), + effectAlignment: .center, + action: { + component.resellGift(true) + }, + animateScale: false ), - effectAlignment: .center, - action: { - component.resellGift(true) - }, - animateScale: false - ), - availableSize: CGSize(width: 120.0, height: 30.0), - transition: context.transition - ) - context.add(priceButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) - } - - if !incoming, let _ = resellStars { + availableSize: CGSize(width: 150.0, height: 30.0), + transition: context.transition + ) + context.add(priceButton + .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { } else { selling = true @@ -2361,7 +2357,7 @@ private final class GiftViewSheetContent: CombinedComponent { let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration)) var canWear = true - if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel { + if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel { canWear = false buttonContent = AnyComponentWithIdentity( id: AnyHashable("wear_channel"), @@ -2421,7 +2417,7 @@ private final class GiftViewSheetContent: CombinedComponent { if isChannelGift { state.levelsDisposable.set(combineLatest( queue: Queue.mainQueue(), - context.engine.peers.getChannelBoostStatus(peerId: ownerPeerId), + context.engine.peers.getChannelBoostStatus(peerId: wearOwnerPeerId), context.engine.peers.getMyBoostStatus() ).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in guard let controller, let boostStatus, let myBoostStatus else { @@ -2429,7 +2425,7 @@ private final class GiftViewSheetContent: CombinedComponent { } component.cancel(true) - let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: ownerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil) + let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: wearOwnerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil) controller.push(levelsController) HapticFeedback().impact(.light) @@ -2763,6 +2759,11 @@ private final class GiftViewSheetComponent: CombinedComponent { if let controller = controller() as? GiftViewScreen { controller.dismissAllTooltips() } + }, + willDismiss: { + if let controller = controller() as? GiftViewScreen { + controller.dismissBalanceOverlay() + } } ), environment: { @@ -2901,6 +2902,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let updateSubject = ActionSlot() public var disposed: () -> Void = {} + public var onBuySuccess: () -> Void = {} fileprivate var showBalance = false { didSet { @@ -2927,7 +2929,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.context = context self.subject = subject - var openPeerImpl: ((EnginePeer) -> Void)? + var openPeerImpl: ((EnginePeer, Bool) -> Void)? var openAddressImpl: ((String) -> Void)? var copyAddressImpl: ((String) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)? @@ -2950,7 +2952,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { context: context, subject: subject, openPeer: { peerId in - openPeerImpl?(peerId) + openPeerImpl?(peerId, false) }, openAddress: { address in openAddressImpl?(address) @@ -3009,21 +3011,27 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.navigationPresentation = .flatModal self.automaticallyControlPresentationContextLayout = false - openPeerImpl = { [weak self] peer in + openPeerImpl = { [weak self] peer, gifts in guard let self, let navigationController = self.navigationController as? NavigationController else { return } self.dismissAllTooltips() - let _ = (context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) - ) - |> deliverOnMainQueue).start(next: { peer in - guard let peer else { - return + if gifts { + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + self.push(controller) } + } else { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) - }) + } } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -3379,7 +3387,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let peer else { return } - openPeerImpl?(peer) + openPeerImpl?(peer, false) Queue.mainQueue().after(0.6) { self?.dismiss(animated: false, completion: nil) } @@ -3397,12 +3405,15 @@ public class GiftViewScreen: ViewControllerComponentContainer { } resellGiftImpl = { [weak self] update in - guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else { + guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { return } self.dismissAllTooltips() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))" + //TODO:localize if let resellStars = gift.resellStars, resellStars > 0, !update { let alertController = textAlertController( @@ -3415,10 +3426,16 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } - self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil)))) + switch self.subject { + case let .profileGift(peerId, currentSubject): + self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil)))) + case let .uniqueGift(_, recipientPeerId): + self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId) + default: + break + } + self.onBuySuccess() - let giftTitle = "\(gift.title) #\(gift.number)" - let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text = "\(giftTitle) is removed from sale." let tooltipController = UndoOverlayController( presentationData: presentationData, @@ -3442,7 +3459,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { if let updateResellStars { updateResellStars(nil) } else { - let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: nil) + let reference = arguments.reference ?? .slug(slug: gift.slug) + let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil) |> deliverOnMainQueue).startStandalone() } }), @@ -3458,16 +3476,20 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } - self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price)))) - - let giftTitle = "\(gift.title) #\(gift.number)" - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + switch self.subject { + case let .profileGift(peerId, currentSubject): + self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price)))) + case let .uniqueGift(_, recipientPeerId): + self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId) + default: + break + } + var text = "\(giftTitle) is now for sale!" if update { - text = "\(giftTitle) is relisted for \(price) Stars." + text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars." } - - + let tooltipController = UndoOverlayController( presentationData: presentationData, content: .universalImage( @@ -3490,7 +3512,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { if let updateResellStars { updateResellStars(price) } else { - let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: price) + let reference = arguments.reference ?? .slug(slug: gift.slug) + let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price) |> deliverOnMainQueue).startStandalone() } }) @@ -3607,6 +3630,28 @@ public class GiftViewScreen: ViewControllerComponentContainer { } } + if let _ = arguments.resellStars, case let .uniqueGift(uniqueGift, recipientPeerId) = subject, let _ = recipientPeerId { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "View in Profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c?.dismiss(completion: nil) + + if case let .peerId(peerId) = uniqueGift.owner { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + openPeerImpl?(peer, true) + Queue.mainQueue().after(0.6) { + self.dismiss(animated: false, completion: nil) + } + }) + } + }))) + } + let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController) }) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 2c9d6e16cf..90f3536f54 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -530,10 +530,11 @@ public final class MediaEditor { } } - public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { + public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false, isStandalone: Bool = false) { self.context = context self.mode = mode self.subject = subject + if let values { self.values = values self.updateRenderChain() @@ -581,6 +582,9 @@ public final class MediaEditor { } self.valuesPromise.set(.single(self.values)) + if isStandalone, let device = MTLCreateSystemDefaultDevice() { + self.renderer.setupForStandaloneDevice(device: device) + } self.renderer.addRenderChain(self.renderChain) if hasHistogram { self.renderer.addRenderPass(self.histogramCalculationPass) @@ -611,7 +615,7 @@ public final class MediaEditor { } public func replaceSource(_ image: UIImage, additionalImage: UIImage?, time: CMTime, mirror: Bool) { - guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice, let texture = loadTexture(image: image, device: device) else { + guard let device = self.renderer.effectiveDevice, let texture = loadTexture(image: image, device: device) else { return } let additionalTexture = additionalImage.flatMap { loadTexture(image: $0, device: device) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 4792af7622..9595d22207 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -125,7 +125,7 @@ final class MediaEditorRenderer { func addRenderPass(_ renderPass: RenderPass) { self.renderPasses.append(renderPass) - if let device = self.renderTarget?.mtlDevice, let library = self.library { + if let device = self.effectiveDevice, let library = self.library { renderPass.setup(device: device, library: library) } } @@ -160,6 +160,14 @@ final class MediaEditorRenderer { self.renderPasses.forEach { $0.setup(device: device, library: library) } } + var effectiveDevice: MTLDevice? { + if let device = self.renderTarget?.mtlDevice { + return device + } else { + return self.device + } + } + private func setup() { guard let device = self.renderTarget?.mtlDevice else { return @@ -180,6 +188,11 @@ final class MediaEditorRenderer { self.commonSetup(device: device) } + func setupForStandaloneDevice(device: MTLDevice) { + self.device = device + self.commonSetup(device: device) + } + func setRate(_ rate: Float) { self.textureSource?.setRate(rate) } @@ -240,15 +253,7 @@ final class MediaEditorRenderer { } func renderFrame() { - let device: MTLDevice? - if let renderTarget = self.renderTarget { - device = renderTarget.mtlDevice - } else if let currentDevice = self.device { - device = currentDevice - } else { - device = nil - } - guard let device = device, + guard let device = self.effectiveDevice, let commandQueue = self.commandQueue, let textureCache = self.textureCache, let commandBuffer = commandQueue.makeCommandBuffer(), @@ -366,7 +371,7 @@ final class MediaEditorRenderer { } func finalRenderedImage(mirror: Bool = false) -> UIImage? { - if let finalTexture = self.resultTexture, let device = self.renderTarget?.mtlDevice { + if let finalTexture = self.resultTexture, let device = self.effectiveDevice { return getTextureImage(device: device, texture: finalTexture, mirror: mirror) } else { return nil diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift deleted file mode 100644 index 7310f6d9a3..0000000000 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import UIKit -import Display -import SwiftSignalKit -import Postbox -import TelegramCore -import AccountContext -import TextFormat - -public extension MediaEditorScreenImpl { - static func makeEditVideoCoverController( - context: AccountContext, - video: MediaEditorScreenImpl.Subject, - completed: @escaping () -> Void = {}, - willDismiss: @escaping () -> Void = {}, - update: @escaping (Disposable?) -> Void - ) -> MediaEditorScreenImpl? { - let controller = MediaEditorScreenImpl( - context: context, - mode: .storyEditor, - subject: .single(video), - isEditing: true, - isEditingCover: true, - forwardSource: nil, - initialCaption: nil, - initialPrivacy: nil, - initialMediaAreas: nil, - initialVideoPosition: 0.0, - transitionIn: .noAnimation, - transitionOut: { finished, isNew in - return nil - }, - completion: { result, commit in - if let _ = result.coverTimestamp { - - } - commit({}) - } - ) - controller.willDismiss = willDismiss - controller.navigationPresentation = .flatModal - - return controller - } -} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 1b053546ee..41904b054d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -122,7 +122,10 @@ public extension MediaEditorScreenImpl { return transitionOut } }, - completion: { result, commit in + completion: { results, commit in + guard let result = results.first else { + return + } let entities = generateChatInputTextEntities(result.caption) if repost { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index ef0262ea56..4edda83f7c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -338,7 +338,7 @@ final class MediaEditorScreenComponent: Component { private var isEditingCaption = false private var currentInputMode: MessageInputPanelComponent.InputMode = .text - private var isSelectionPanelOpen = false + fileprivate var isSelectionPanelOpen = false private var didInitializeInputMediaNodeDataPromise = false private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? @@ -2013,10 +2013,21 @@ final class MediaEditorScreenComponent: Component { ) ), effectAlignment: .center, - action: { [weak self] in - if let self { + action: { [weak self, weak controller] in + if let self, let controller { self.isSelectionPanelOpen = !self.isSelectionPanelOpen + if let mediaEditor = controller.node.mediaEditor { + if self.isSelectionPanelOpen { + mediaEditor.maybePauseVideo() + } else { + Queue.mainQueue().after(0.1) { + mediaEditor.maybeUnpauseVideo() + } + } + } self.state?.updated() + + controller.hapticFeedback.impact(.light) } }, animateAlpha: false @@ -2034,8 +2045,8 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) - transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) + transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01) + transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0) if self.isSelectionPanelOpen { let selectionPanelFrame = CGRect( @@ -2061,10 +2072,12 @@ final class MediaEditorScreenComponent: Component { return } self.isSelectionPanelOpen = false - self.state?.updated() + self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate) if let id { controller.node.switchToItem(id) + + controller.hapticFeedback.impact(.light) } }, itemSelectionToggled: { [weak self, weak controller] id in @@ -2088,6 +2101,8 @@ final class MediaEditorScreenComponent: Component { controller.node.items[fromIndex] = toItem controller.node.items[toIndex] = fromItem self.state?.updated(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.tap() } ) ), @@ -2104,7 +2119,7 @@ final class MediaEditorScreenComponent: Component { selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) } } else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in selectionPanelView?.removeFromSuperview() }) @@ -4027,7 +4042,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } if gestureRecognizer === self.dismissPanGestureRecognizer { let location = gestureRecognizer.location(in: self.entitiesView) - if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil { + if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil || self.componentHostView?.isSelectionPanelOpen == true { return false } return true @@ -4188,7 +4203,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID private var previousRotateTimestamp: Double? @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard !self.isCollageTimelineOpen else { + guard !self.isCollageTimelineOpen && !(self.componentHostView?.isSelectionPanelOpen ?? false) else { return } if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection { @@ -5381,7 +5396,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var updatedCurrentItem = self.items[currentItemIndex] updatedCurrentItem.caption = self.getCaption() - if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values { + if (mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values) || updatedCurrentItem.values?.gradientColors == nil { updatedCurrentItem.values = mediaEditor.values updatedCurrentItem.version += 1 @@ -6520,7 +6535,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID public var cancelled: (Bool) -> Void = { _ in } public var willComplete: (UIImage?, Bool, @escaping () -> Void) -> Void - public var completion: (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + public var completion: ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void public var dismissed: () -> Void = { } public var willDismiss: () -> Void = { } public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? @@ -6529,7 +6544,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID private var closeFriends = Promise<[EnginePeer]>() private let storiesBlockedPeers: BlockedPeersContext - private let hapticFeedback = HapticFeedback() + fileprivate let hapticFeedback = HapticFeedback() private var audioSessionDisposable: Disposable? private let postingAvailabilityPromise = Promise() @@ -6554,7 +6569,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, willComplete: @escaping (UIImage?, Bool, @escaping () -> Void) -> Void = { _, _, commit in commit() }, - completion: @escaping (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + completion: @escaping ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context self.mode = mode @@ -6977,7 +6992,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let hasPremium = self.context.isPremium let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) - let title = presentationData.strings.Story_Editor_ExpirationText let currentValue = self.state.privacy.timeout let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil @@ -6994,62 +7008,56 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID ) } - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + let timeoutOptions: [(hours: Int, requiresPremium: Bool)] = [ + (6, true), + (12, true), + (24, false), + (48, true) + ] - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(6), icon: { theme in - if !hasPremium { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) - } else { - return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - } - }, action: { [weak self] _, a in - a(.default) - - if hasPremium { - updateTimeout(3600 * 6) - } else { - self?.presentTimeoutPremiumSuggestion() - } - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(12), icon: { theme in - if !hasPremium { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) - } else { - return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - } - }, action: { [weak self] _, a in - a(.default) - - if hasPremium { - updateTimeout(3600 * 12) - } else { - self?.presentTimeoutPremiumSuggestion() - } - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(24), icon: { theme in - return currentValue == 86400 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) - - updateTimeout(86400) - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(48), icon: { theme in - if !hasPremium { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) - } else { - return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - } - }, action: { [weak self] _, a in - a(.default) - - if hasPremium { - updateTimeout(86400 * 2) - } else { - self?.presentTimeoutPremiumSuggestion() - } - }))) + var items: [ContextMenuItem] = [ + .action(ContextMenuActionItem( + text: presentationData.strings.Story_Editor_ExpirationText, + textLayout: .multiline, + textFont: .small, + icon: { _ in return nil }, + action: emptyAction + )) + ] + for option in timeoutOptions { + let text = presentationData.strings.Story_Editor_ExpirationValue(Int32(option.hours)) + let value = option.hours * 3600 + + items.append(.action(ContextMenuActionItem( + text: text, + icon: { theme in + if option.requiresPremium && !hasPremium { + return generateTintedImage( + image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), + color: theme.contextMenu.secondaryColor + ) + } else if currentValue == value { + return generateTintedImage( + image: UIImage(bundleImageName: "Chat/Context Menu/Check"), + color: theme.contextMenu.primaryColor + ) + } else { + return nil + } + }, + action: { [weak self] _, a in + a(.default) + + if !option.requiresPremium || hasPremium { + updateTimeout(value) + } else { + self?.presentTimeoutPremiumSuggestion() + } + } + ))) + } + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } @@ -7332,30 +7340,335 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } return true } - - private var didComplete = false - func requestStoryCompletion(animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { + + private func completeWithMultipleResults(results: [MediaEditorScreenImpl.Result]) { + // Send all results to completion handler + self.completion(results, { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) + } + + private func processMultipleItems() { + guard !self.node.items.isEmpty else { return } - self.didComplete = true - - self.dismissAllTooltips() - - mediaEditor.stop() - mediaEditor.invalidate() - self.node.entitiesView.invalidate() - - let context = self.context - if let navigationController = self.navigationController as? NavigationController { - navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = self.node.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) { + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } + let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + + var updatedCurrentItem = self.node.items[currentItemIndex] + updatedCurrentItem.caption = self.node.getCaption() + updatedCurrentItem.values = mediaEditor.values + self.node.items[currentItemIndex] = updatedCurrentItem } + let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: []) + let totalItems = self.node.items.count + + let dispatchGroup = DispatchGroup() + + let privacy = self.state.privacy + + if !(self.isEditingStory || self.isEditingStoryCover) { + let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in + if let current { + return current.withUpdatedPrivacy(privacy) + } else { + return MediaEditorStoredState(privacy: privacy, textSettings: nil) + } + }).start() + } + + var order: [Int64] = [] + for (index, item) in self.node.items.enumerated() { + guard item.isEnabled else { + continue + } + + dispatchGroup.enter() + + let randomId = Int64.random(in: .min ... .max) + order.append(randomId) + + if item.asset.mediaType == .video { + processVideoItem(item: item, index: index, randomId: randomId) { result in + let _ = multipleResults.modify { results in + var updatedResults = results + updatedResults.append(result) + return updatedResults + } + + dispatchGroup.leave() + } + } else if item.asset.mediaType == .image { + processImageItem(item: item, index: index, randomId: randomId) { result in + let _ = multipleResults.modify { results in + var updatedResults = results + updatedResults.append(result) + return updatedResults + } + + dispatchGroup.leave() + } + } else { + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + let results = multipleResults.with { $0 } + if results.count == totalItems { + var orderedResults: [MediaEditorScreenImpl.Result] = [] + for id in order { + if let item = results.first(where: { $0.randomId == id }) { + orderedResults.append(item) + } + } + self.completeWithMultipleResults(results: orderedResults) + } + } + } + + private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) { + let asset = item.asset + + let itemMediaEditor = setupMediaEditorForItem(item: item) + + var caption = item.caption + caption = convertMarkdownToAttributes(caption) + + var mediaAreas: [MediaArea] = [] + var stickers: [TelegramMediaFile] = [] + + if let entities = item.values?.entities { + for entity in entities { + if let mediaArea = entity.mediaArea { + mediaAreas.append(mediaArea) + } + + // Extract stickers from entities + extractStickersFromEntity(entity, into: &stickers) + } + } + + // Process video + let firstFrameTime: CMTime + if let coverImageTimestamp = item.values?.coverImageTimestamp { + firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) + } else { + firstFrameTime = .zero + } + + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in + guard let avAsset else { + DispatchQueue.main.async { + if let self { + completion(self.createEmptyResult(randomId: randomId)) + } + } + return + } + + // Calculate duration + let duration: Double + if let videoTrimRange = item.values?.videoTrimRange { + duration = videoTrimRange.upperBound - videoTrimRange.lowerBound + } else { + duration = min(asset.duration, storyMaxVideoDuration) + } + + // Generate thumbnail frame + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in + guard let self else { + return + } + DispatchQueue.main.async { + if let cgImage { + let image = UIImage(cgImage: cgImage) + itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false) + + if let resultImage = itemMediaEditor.resultImage { + makeEditorImageComposition( + context: self.node.ciContext, + postbox: self.context.account.postbox, + inputImage: resultImage, + dimensions: storyDimensions, + values: itemMediaEditor.values, + time: firstFrameTime, + textScale: 2.0 + ) { coverImage in + if let coverImage = coverImage { + let result = MediaEditorScreenImpl.Result( + media: .video( + video: .asset(localIdentifier: asset.localIdentifier), + coverImage: coverImage, + values: itemMediaEditor.values, + duration: duration, + dimensions: itemMediaEditor.values.resultDimensions + ), + mediaAreas: mediaAreas, + caption: caption, + coverTimestamp: itemMediaEditor.values.coverImageTimestamp, + options: self.state.privacy, + stickers: stickers, + randomId: randomId + ) + completion(result) + } else { + completion(self.createEmptyResult(randomId: randomId)) + } + } + } else { + completion(self.createEmptyResult(randomId: randomId)) + } + } else { + completion(self.createEmptyResult(randomId: randomId)) + } + } + } + } + } + + private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) { + let asset = item.asset + + // Setup temporary media editor for this item + let itemMediaEditor = setupMediaEditorForItem(item: item) + + // Get caption for this item + var caption = item.caption + caption = convertMarkdownToAttributes(caption) + + // Media areas and stickers + var mediaAreas: [MediaArea] = [] + var stickers: [TelegramMediaFile] = [] + + if let entities = item.values?.entities { + for entity in entities { + if let mediaArea = entity.mediaArea { + mediaAreas.append(mediaArea) + } + + // Extract stickers from entities + extractStickersFromEntity(entity, into: &stickers) + } + } + + // Request full-size image + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = true + + PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in + guard let self else { + return + } + DispatchQueue.main.async { + if let image { + itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false) + + if let resultImage = itemMediaEditor.resultImage { + makeEditorImageComposition( + context: self.node.ciContext, + postbox: self.context.account.postbox, + inputImage: resultImage, + dimensions: storyDimensions, + values: itemMediaEditor.values, + time: .zero, + textScale: 2.0 + ) { resultImage in + if let resultImage = resultImage { + let result = MediaEditorScreenImpl.Result( + media: .image( + image: resultImage, + dimensions: PixelDimensions(resultImage.size) + ), + mediaAreas: mediaAreas, + caption: caption, + coverTimestamp: nil, + options: self.state.privacy, + stickers: stickers, + randomId: randomId + ) + completion(result) + } else { + completion(self.createEmptyResult(randomId: randomId)) + } + } + } else { + completion(self.createEmptyResult(randomId: randomId)) + } + } else { + completion(self.createEmptyResult(randomId: randomId)) + } + } + } + } + + private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor { + return MediaEditor( + context: self.context, + mode: .default, + subject: .asset(item.asset), + values: item.values, + hasHistogram: false, + isStandalone: true + ) + } + + private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) { + switch entity { + case let .sticker(stickerEntity): + if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType { + stickers.append(file.media) + } + case let .text(textEntity): + if let subEntities = textEntity.renderSubEntities { + for entity in subEntities { + if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType { + stickers.append(file.media) + } + } + } + default: + break + } + } + + private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result { + let emptyImage = UIImage() + return MediaEditorScreenImpl.Result( + media: .image( + image: emptyImage, + dimensions: PixelDimensions(emptyImage.size) + ), + mediaAreas: [], + caption: NSAttributedString(), + coverTimestamp: nil, + options: self.state.privacy, + stickers: [], + randomId: randomId + ) + } + + private func processSingleItem() { + guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else { + return + } + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - + var caption = self.node.getCaption() caption = convertMarkdownToAttributes(caption) @@ -7407,7 +7720,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { self.saveDraft(id: randomId, isEdit: true) - self.completion(MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -7737,7 +8050,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") - self.completion(MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -7754,38 +8067,70 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if case let .draft(draft, id) = actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) } - } else { - if let image = mediaEditor.resultImage { - self.saveDraft(id: randomId) - - var values = mediaEditor.values - var outputDimensions: CGSize? - if case .avatarEditor = self.mode { - outputDimensions = CGSize(width: 640.0, height: 640.0) - values = values.withUpdatedQualityPreset(.profile) - } - makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: outputDimensions, values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in - if let self, let resultImage { - self.willComplete(resultImage, false, { [weak self] in - guard let self else { - return - } - Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - self.completion(MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in - self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in - self?.dismiss() - Queue.mainQueue().justDispatch { - finished() - } - }) - }) - if case let .draft(draft, id) = actualSubject, id == nil { - removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) - } - }) - } - }) + } else if let image = mediaEditor.resultImage { + self.saveDraft(id: randomId) + + var values = mediaEditor.values + var outputDimensions: CGSize? + if case .avatarEditor = self.mode { + outputDimensions = CGSize(width: 640.0, height: 640.0) + values = values.withUpdatedQualityPreset(.profile) } + makeEditorImageComposition( + context: self.node.ciContext, + postbox: self.context.account.postbox, + inputImage: image, + dimensions: storyDimensions, + outputDimensions: outputDimensions, + values: values, + time: .zero, + textScale: 2.0, + completion: { [weak self] resultImage in + if let self, let resultImage { + self.willComplete(resultImage, false, { [weak self] in + guard let self else { + return + } + Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") + self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) + if case let .draft(draft, id) = actualSubject, id == nil { + removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) + } + }) + } + }) + } + } + + private var didComplete = false + func requestStoryCompletion(animated: Bool) { + guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { + return + } + + self.didComplete = true + + self.dismissAllTooltips() + + mediaEditor.stop() + mediaEditor.invalidate() + self.node.entitiesView.invalidate() + + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + + if self.node.items.count(where: { $0.isEnabled }) > 1 { + self.processMultipleItems() + } else { + self.processSingleItem() } } @@ -7852,7 +8197,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } #endif - self.completion(MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size))), { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)))], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -7955,7 +8300,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if isVideo { self.uploadSticker(file, action: .send) } else { - self.completion(MediaEditorScreenImpl.Result( + self.completion([MediaEditorScreenImpl.Result( media: .sticker(file: file, emoji: self.effectiveStickerEmoji()), mediaAreas: [], caption: NSAttributedString(), @@ -7963,7 +8308,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [], randomId: 0 - ), { [weak self] finished in + )], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -8376,7 +8721,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID result = MediaEditorScreenImpl.Result() } - self.completion(result, { [weak self] finished in + self.completion([result], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in guard let self else { return diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift index b405b061f7..b915534b4f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift @@ -145,23 +145,11 @@ final class SelectionPanelComponent: Component { selectionLayer.lineWidth = lineWidth selectionLayer.frame = selectionFrame selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) - -// if !transition.animation.isImmediate { -// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) -// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) -// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2) -// } } } else if let selectionLayer = self.selectionLayer { self.selectionLayer = nil selectionLayer.removeFromSuperlayer() - -// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) -// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) -// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in -// selectionLayer.removeFromSuperlayer() -// }) } } } @@ -373,11 +361,96 @@ final class SelectionPanelComponent: Component { } func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) { + guard let component = self.component else { + return + } + self.scrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + let buttonFrame = buttonView.convert(buttonView.bounds, to: self) + let fromPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y) + + self.scrollView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + self.scrollView.layer.animateBounds(from: CGRect(origin: CGPoint(x: buttonFrame.minX - self.scrollView.frame.minX, y: buttonFrame.minY - self.scrollView.frame.minY), size: buttonFrame.size), to: self.scrollView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + self.backgroundMaskPanelView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(16.5)), to: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4) + self.backgroundMaskPanelView.layer.animateBounds(from: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), to: self.backgroundMaskPanelView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + let mainCircleDelay: Double = 0.02 + let backgroundWidth = self.backgroundMaskPanelView.frame.width + for item in component.items { + guard let itemView = self.itemViews[item.asset.localIdentifier] else { + continue + } + + let distance = abs(itemView.frame.center.x - backgroundWidth) + let distanceNorm = distance / backgroundWidth + let adjustedDistanceNorm = distanceNorm + let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.14 + + itemView.isHidden = true + Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in + guard let itemView else { + return + } + itemView.isHidden = false + itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } } func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) { - completion() + guard let component = self.component else { + completion() + return + } + + self.scrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + let buttonFrame = buttonView.convert(buttonView.bounds, to: self) + let scrollButtonFrame = buttonView.convert(buttonView.bounds, to: self.scrollView) + let toPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y) + + self.scrollView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + self.scrollView.layer.animateBounds(from: self.scrollView.bounds, to: CGRect(origin: CGPoint(x: (buttonFrame.minX - self.scrollView.frame.minX) / 2.0, y: (buttonFrame.minY - self.scrollView.frame.minY) / 2.0), size: buttonFrame.size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + self.backgroundMaskPanelView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), to: NSNumber(value: Float(16.5)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, removeOnCompletion: false) + self.backgroundMaskPanelView.layer.animateBounds(from: self.backgroundMaskPanelView.bounds, to: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { finished in + if finished { + completion() + self.backgroundMaskPanelView.layer.removeAllAnimations() + for (_, itemView) in self.itemViews { + itemView.layer.removeAllAnimations() + } + } + }) + + let mainCircleDelay: Double = 0.0 + let backgroundWidth = self.backgroundMaskPanelView.frame.width + + for item in component.items { + guard let itemView = self.itemViews[item.asset.localIdentifier] else { + continue + } + let distance = abs(itemView.frame.center.x - backgroundWidth) + let distanceNorm = distance / backgroundWidth + let adjustedDistanceNorm = distanceNorm + + let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.05 + + Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in + guard let itemView else { + return + } + + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + itemView.layer.animatePosition(from: itemView.center, to: scrollButtonFrame.center, duration: 0.4) + } } func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index c25ad5dee1..00f309ace7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -201,7 +201,10 @@ extension PeerInfoScreenImpl { commit() } }, - completion: { [weak self] result, commit in + completion: { [weak self] results, commit in + guard let result = results.first else { + return + } switch result.media { case let .image(image, _): resultImage = image @@ -217,7 +220,7 @@ extension PeerInfoScreenImpl { break } dismissImpl?() - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 3ca1bfee0b..1b70f7765d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -607,10 +607,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return self.profileGifts.buyStarGift(slug: slug, peerId: peerId) }, updateResellStars: { [weak self] price in - guard let self, case let .unique(uniqueGift) = product.gift else { + guard let self, let reference = product.reference else { return } - self.profileGifts.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) + self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) }, togglePinnedToTop: { [weak self] pinnedToTop in guard let self else { @@ -1479,6 +1479,8 @@ private extension StarGiftReference { return "m_\(messageId.id)" case let .peer(peerId, id): return "p_\(peerId.toInt64())_\(id)" + case let .slug(slug): + return "s_\(slug)" } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 94670977dc..9540acbeab 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -1315,13 +1315,13 @@ extension ChatControllerImpl { ) } return nil - }, completion: { result, commit in - if case let .image(image, _) = result.media { + }, completion: { results, commit in + if case let .image(image, _) = results.first?.media { completion(image) commit({}) } dismissImpl?() - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() @@ -1930,17 +1930,17 @@ extension ChatControllerImpl { ) } return nil - }, completion: { [weak self] result, commit in + }, completion: { [weak self] results, commit in dismissImpl?() self?.chatDisplayNode.dismissInput() Queue.mainQueue().after(0.1) { commit({}) - if case let .sticker(file, _) = result.media { + if case let .sticker(file, _) = results.first?.media { self?.enqueueStickerFile(file) } } - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d234859ace..450afd568b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3461,9 +3461,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { ) } return nil - }, completion: { result, commit in - completion(result, commit) - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + }, completion: { results, commit in + completion(results.first!, commit) + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() @@ -3525,13 +3525,13 @@ public final class SharedAccountContextImpl: SharedAccountContext { ) } return nil - }, completion: { result, commit in - if case let .sticker(file, emoji) = result.media { + }, completion: { results, commit in + if case let .sticker(file, emoji) = results.first?.media { completion(file, emoji, { commit({}) }) } - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() @@ -3558,13 +3558,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { transitionIn: nil, transitionOut: { finished, isNew in return nil - }, completion: { result, commit in - completion(result, commit) - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + }, completion: { results, commit in + completion(results.first!, commit) + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) -// editorController.cancelled = { _ in -// cancelled() -// } return editorController } @@ -3724,7 +3721,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { transitionOut: { _, _ in return nil }, - completion: { [weak parentController] result, commit in + completion: { [weak parentController] results, commit in + guard let result = results.first else { + return + } let targetPeerId: EnginePeer.Id let target: Stories.PendingTarget if let sendAsPeerId = result.options.sendAsPeerId { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 3a6662cddd..7c6bff3c7c 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -444,7 +444,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } else { return nil } - }, completion: { [weak self] result, commit in + }, completion: { [weak self] results, commit in guard let self else { dismissCameraImpl?() commit({}) @@ -453,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let customTarget, case .botPreview = customTarget { externalState.storyTarget = customTarget - self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + self.proceedWithStoryUpload(target: customTarget, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) dismissCameraImpl?() return @@ -464,7 +464,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon target = .peer(id) targetPeerId = id } else { - if let sendAsPeerId = result.options.sendAsPeerId { + if let sendAsPeerId = results.first?.options.sendAsPeerId { target = .peer(sendAsPeerId) targetPeerId = sendAsPeerId } else { @@ -486,12 +486,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon externalState.isPeerArchived = channel.storiesHidden ?? false } - self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + self.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) dismissCameraImpl?() }) } - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) controller.cancelled = { showDraftTooltip in if showDraftTooltip {